打通es及lucene应用,lucene应用es Query,queryString Query获取及标准化
打通es及lucene应用,lucene应用es Query,queryString Query获取及标准化
https://github.com/cclient/elasticsearch-query-string-lucene-format
最终目标是在外部程序内构建lucene索引,并保证和es查询的兼容性(主要是全文检索部分),通过异构存储加载doc计算检索命中,减少es的压力
调研思路
-
基本示例
看到这篇文章的,应该都有一定的es/lucene/大数据应用经验,很多概念也不好作更细的说明
lucene索引的建立和检索,可以先看官方和第三方应用两个基本示例,预先有些了解
官方
https://lucene.apache.org/core/7_7_3/demo/src-html/org/apache/lucene/demo/IndexFiles.html
https://lucene.apache.org/core/7_7_3/demo/src-html/org/apache/lucene/demo/SearchFiles.html
第三方
-
lucene和es的兼容性问题
索引的建立比较简单
为简化问题,我们只考虑单分词字段(实际会用到全部的分词字段),实际我最初的目标也只是做针对分词字段,因为非分词字段,不论在hive/hbase/spark sql还是代码里实现逻辑过滤都不难,因此并没有考虑如何应用es内的其他term/range类查询
但即使是对单分词field,es 对分词字段的query_string 和lucene 也并不完全兼容,即通过es search query_string 语法的查询字符串,直接应用在lucene上,命中结果并不一致
脱离es环境,在大数据分布式场景下,直接应用lucene,并保持和es分词一致
分别在两个方向进行调研
-
1 从lucene向上,研究lucene的语法规则,把es的query string 转化为标准的lucene
-
从es往下,es基于lucene作检索,必然有语法格式化相关的代码,因此如何定位和提取到es真正应用lucene的query对应,提取该对象应用在lucene上
目前两个方向都搞定了
方向一,测试结果ok,挑先出的测试用例都验证证确,保留态度
方向二,测试结果ok,完美解决
再强调,以下只是对单分词字段的应用,包含其他term,range等查询的条件并不涉及
提取query-string Query对象调研过程
代码量很小,实际工作量都在查看/逆向/拼接/提取 es代码上,其实工作状态和很早期做各种web/app爬虫逆向破解类似,但比爬虫这些简单多了,至少源码无混淆,ide跳转方遍,工程化特征明显(比如测试用例),结合es的使用经验,很方遍理解和定位特征
这是两年前的代码了,当时es大版本为6.8 因此以下内容都针对es 6.8版本,7以上版本,对本文来说,基本没有影响,有时间再看看7的方案
整体上7的变化主要是新功能支持(大部分还是xpack) 对历史的至少index search 部分影响不大
另外,殊途同归,需求/目标不一致,逆向路径不同,个人经验不同,获取的信息不同,结果可能也不一致,不排除有更好的办法,这里只是抛砖引玉
QueryString Query对象获取
因为目标是对分词文本的检索,因此把目标锁定在QueryString
首先查看代码结构,并通过关键词QueryStringQuery,定位到两个关键类
QueryStringQueryBuilder 和 QueryStringQueryParser
QueryStringQueryBuilder
会调用 org.elasticsearch.index.search.QueryStringQueryParser
目标锁定至QueryStringQueryParser,QueryStringQueryParser该类下有大量方法反回query对象,重点关注的是parse方法,熟悉lucene的知道,这个是lucene的同名方法
es QueryStringQueryParser内的parse方法
lucene下QueryParserBase的同名基础parse方法,可以看到,包属于lucene
实际es QueryStringQueryParser是lucene QueryParserBase的子类,继承关系如下
public class QueryStringQueryParser extends XQueryParser {
public class XQueryParser extends QueryParser {
public class QueryParser extends QueryParserBase implements QueryParserConstants {
@Override
public Query parse(String query) throws ParseException {
if (query.trim().isEmpty()) {
return Queries.newMatchNoDocsQuery("Matching no documents because no terms present");
}
return super.parse(query);
}
其实我的目标是parse方法构造出的Query对象,es的查询语句query_string部分,通过QueryStringQueryParser 执行parse,返回的Query对象,拿到这个Query对象最好能直接应到到lucene的查询上,如果无法应用,则若能转为标准的lucene语法,后续可应到到Lucene上
实际到这里目的已经明确了,至少能看到希望了
实始化QueryStringQueryParser,再调用parse(String query)方法即可
但比较可惜的是QueryStringQueryParser的几个构造方法需要额外的几项参数,首当其冲的就是QueryShardContext 这个对象
先看看 QueryShardContext 这个类
看看这构造函数,除了一个重载的复制构造方法,真正的构造方法需要大量参数,也基本都是es集群的配置信息,这是正式启动es服务时,es服务应用的类
public class QueryShardContext extends QueryRewriteContext {
private final ScriptService scriptService;
private final IndexSettings indexSettings;
private final MapperService mapperService;
private final SimilarityService similarityService;
private final BitsetFilterCache bitsetFilterCache;
private final Function<IndexReaderContext, IndexSearcher> searcherFactory;
private final BiFunction<MappedFieldType, String, IndexFieldData<?>> indexFieldDataService;
private final int shardId;
private final IndexReader reader;
private final String clusterAlias;
private String[] types = Strings.EMPTY_ARRAY;
private boolean cacheable = true;
private final SetOnce<Boolean> frozen = new SetOnce<>();
private final Index fullyQualifiedIndex;
public void setTypes(String... types) {
this.types = types;
}
public String[] getTypes() {
return types;
}
private final Map<String, Query> namedQueries = new HashMap<>();
private boolean allowUnmappedFields;
private boolean mapUnmappedFieldAsString;
private NestedScope nestedScope;
private boolean isFilter;
public QueryShardContext(int shardId, IndexSettings indexSettings, BitsetFilterCache bitsetFilterCache,
Function<IndexReaderContext, IndexSearcher> searcherFactory,
BiFunction<MappedFieldType, String, IndexFieldData<?>> indexFieldDataLookup, MapperService mapperService,
SimilarityService similarityService, ScriptService scriptService, NamedXContentRegistry xContentRegistry,
NamedWriteableRegistry namedWriteableRegistry, Client client, IndexReader reader, LongSupplier nowInMillis,
String clusterAlias) {
super(xContentRegistry, namedWriteableRegistry,client, nowInMillis);
this.shardId = shardId;
this.similarityService = similarityService;
this.mapperService = mapperService;
this.bitsetFilterCache = bitsetFilterCache;
this.searcherFactory = searcherFactory;
this.indexFieldDataService = indexFieldDataLookup;
this.allowUnmappedFields = indexSettings.isDefaultAllowUnmappedFields();
this.nestedScope = new NestedScope();
this.scriptService = scriptService;
this.indexSettings = indexSettings;
this.reader = reader;
this.clusterAlias = clusterAlias;
this.fullyQualifiedIndex = new Index(RemoteClusterAware.buildRemoteIndexName(clusterAlias, indexSettings.getIndex().getName()),
indexSettings.getIndex().getUUID());
}
public QueryShardContext(QueryShardContext source) {
this(source.shardId, source.indexSettings, source.bitsetFilterCache, source.searcherFactory,
source.indexFieldDataService, source.mapperService, source.similarityService, source.scriptService,
source.getXContentRegistry(), source.getWriteableRegistry(), source.client, source.reader,
source.nowInMillis, source.clusterAlias);
this.types = source.getTypes();
}
}
构造方法较为复杂,我们找找其他QueryStringQueryParser实例构造的路径,首先想到的是找测试用例
测试用例会有脱离es集群的独立调用,这里应该有构造的所有依赖,我们看看有没有QueryStringQueryParser相关的项
果然找到了,基他类似的Test提前留意下
public void testToQueryWildcardQuery() throws Exception {
assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0);
for (Operator op : Operator.values()) {
BooleanClause.Occur defaultOp = op.toBooleanClauseOccur();
QueryStringQueryParser queryParser = new QueryStringQueryParser(createShardContext(), STRING_FIELD_NAME);
queryParser.setAnalyzeWildcard(true);
queryParser.setMultiTermRewriteMethod(MultiTermQuery.CONSTANT_SCORE_REWRITE);
queryParser.setDefaultOperator(op.toQueryParserOperator());
Query query = queryParser.parse("first foo-bar-foobar* last");
Query expectedQuery =
new BooleanQuery.Builder()
.add(new BooleanClause(new TermQuery(new Term(STRING_FIELD_NAME, "first")), defaultOp))
.add(new BooleanQuery.Builder()
.add(new BooleanClause(new TermQuery(new Term(STRING_FIELD_NAME, "foo")), defaultOp))
.add(new BooleanClause(new TermQuery(new Term(STRING_FIELD_NAME, "bar")), defaultOp))
.add(new BooleanClause(new PrefixQuery(new Term(STRING_FIELD_NAME, "foobar")), defaultOp))
.build(), defaultOp)
.add(new BooleanClause(new TermQuery(new Term(STRING_FIELD_NAME, "last")), defaultOp))
.build();
assertThat(query, Matchers.equalTo(expectedQuery));
}
}
目标代码 QueryStringQueryParser queryParser = new QueryStringQueryParser(createShardContext(), STRING_FIELD_NAME);
通过createShardContext 构造出了QueryShardContext类实例
到这里,理论上这条路就通了
-
1 createShardContext 构造出了QueryShardContext类实例qsc
-
2 通过qsc,构造出QueryStringQueryParser类实例qsqp
-
3 调用qsqp.parse 得到Query对象q
-
4 在lucene 上应用q
分词处理
还有另一重要问题没有解决,对分词字段来说,需要指定该分词应用的分词算法
对正常的es集群服务,通过安装插件,建index,为index配置mapping,在mapping内指定field上应用的分词analyzer,也会指定es query filed的analyzer,查询时,es通过search和mapping内的信息,确定出使用哪个分词analyzer,再应用该analyzer执行计算
但目前调研的方法,这种路径并不通,因为并没有一个完整的es环境,测试用例初始化的QueryShardContext,缺失很多信息
这里的调研方向有两条
- 1 做加法
补全QueryShardContext的信息,尽量使QueryShardContext和真实的es环境一致,加载分词插件,注册mapping等,这种方法凭经验预估,难度会高些,但还是有些蛛丝马迹的
比如QueryShardContext的构造函数里,会有indexSettings,mapperService,应该是indexSettings对应集群配置,mapperService对应es的mapping映射管理,追进代码里看也基本确定。
public QueryShardContext(int shardId, IndexSettings indexSettings, BitsetFilterCache bitsetFilterCache,
Function<IndexReaderContext, IndexSearcher> searcherFactory,
BiFunction<MappedFieldType, String, IndexFieldData<?>> indexFieldDataLookup, MapperService mapperService,
SimilarityService similarityService, ScriptService scriptService, NamedXContentRegistry xContentRegistry,
NamedWriteableRegistry namedWriteableRegistry, Client client, IndexReader reader, LongSupplier nowInMillis,
String clusterAlias) {
super(xContentRegistry, namedWriteableRegistry,client, nowInMillis);
this.shardId = shardId;
this.similarityService = similarityService;
this.mapperService = mapperService;
this.bitsetFilterCache = bitsetFilterCache;
this.searcherFactory = searcherFactory;
this.indexFieldDataService = indexFieldDataLookup;
this.allowUnmappedFields = indexSettings.isDefaultAllowUnmappedFields();
this.nestedScope = new NestedScope();
this.scriptService = scriptService;
this.indexSettings = indexSettings;
this.reader = reader;
this.clusterAlias = clusterAlias;
this.fullyQualifiedIndex = new Index(RemoteClusterAware.buildRemoteIndexName(clusterAlias, indexSettings.getIndex().getName()),
indexSettings.getIndex().getUUID());
}
- 2 做减法
强制指定分词
因为我的目标只是中文分词,并且只会应用一种分词查询analyzer(不会说有两个字段,一个应用analyzer1,另一个应用analyzer2),因此可不可以跳过注册插件,注册mapping的部分,直接通过参数传入analyzer,并跳过从mapping获取analyzer的过程,直接使用传入的analyzer?
预想是【做减法】比【做加法】简单,加法有额外的东西,也就有更多的依赖,减法反而是减少依赖的过程,先试试做减法的路子能不能走通,如果路子难走,再走做加法的路子。
补充一点,以上只是经验之谈,实际也可能做加法更容易,做减法也容易引入一些其他问题
例如,加法可能会很方便的找到缺失信息,简单就能补全。减法,如果少了一些信息,虽然可能最后走通了,但可能因为缺失信息,结果不达预期,还要掉头来补上,或放弃,重花时间走加法。
加法也是更完美的办法
最终比较顺利的,以做减法的方式,调通了
调整主要是以下三项
-
1 添加了一个QueryStringQueryParser 构造方法,添加参数Analyzer analyzer,这个参数会传入我真正期望的中文分词analyzer
-
2 替换官方的 extractMultiFields方法,这个方法看起来是,通过具体的mapping 获取信息,但我这里压根就没有mapping,方法返回值需要Map<String, Float>,我先只返回个单字段的mapping即可,看看效果
-
3 更改代码内的analyzer 为我传入的analyzer
Analyzer oldAnalyzer = queryBuilder.analyzer
public class QueryStringQueryParser extends XQueryParser {
//因为个的场景只使用一种分词算法,所以使用了全局的静态变量,必要的话可以改为实例变量,结构不影响,对本文来说,只是保存传入analyzer
public static Analyzer defaultAnalyzer;
static {
defaultAnalyzer = new StandardAnalyzer();
}
//传入analyzer
public QueryStringQueryParser(QueryShardContext context, String defaultField, Analyzer analyzer) {
this(context, defaultField, Collections.emptyMap(), false, analyzer);
QueryStringQueryParser.defaultAnalyzer = analyzer;
MatchQuery.defaultAnalyzer = analyzer;
}
// 这里官方原生的的extractMultiFields,可以大概看到是从resolveMappingField 解析
// private Map<String, Float> extractMultiFields(String field, boolean quoted) {
// if (field != null) {
// boolean allFields = Regex.isMatchAllPattern(field);
// if (allFields && this.field != null && this.field.equals(field)) {
// // "*" is the default field
// return fieldsAndWeights;
// }
// boolean multiFields = Regex.isSimpleMatchPattern(field);
// // Filters unsupported fields if a pattern is requested
// // Filters metadata fields if all fields are requested
// return resolveMappingField(context, field, 1.0f, !allFields, !multiFields, quoted ? quoteFieldSuffix : null);
// } else if (quoted && quoteFieldSuffix != null) {
// return resolveMappingFields(context, fieldsAndWeights, quoteFieldSuffix);
// } else {
// return fieldsAndWeights;
// }
// }
// 个人重写的,因为只对单字段应用分词,直接指定一个field,这个filed的名称需要注意,如果后期使用正则替换的话
private Map<String, Float> extractMultiFields(String field, boolean quoted) {
Map<String, Float> fields = new HashMap<>();
fields.put(field, 1.0f);
//todo ...
return fields;
}
@Override
protected Query getFieldQuery(String field, String queryText, int slop) throws ParseException {
if (field != null && EXISTS_FIELD.equals(field)) {
return existsQuery(queryText);
}
Map<String, Float> fields = extractMultiFields(field, true);
if (fields.isEmpty()) {
return newUnmappedFieldQuery(field);
}
// 官方原生的使用的是queryBuilder.analyzer
// Analyzer oldAnalyzer = queryBuilder.analyzer;
// 为减小复杂度,改为指定传入的
Analyzer oldAnalyzer = defaultAnalyzer;
// int oldSlop = queryBuilder.phraseSlop;
int oldSlop = 0;
try {
if (forceQuoteAnalyzer != null) {
queryBuilder.setAnalyzer(forceQuoteAnalyzer);
} else if (forceAnalyzer != null) {
queryBuilder.setAnalyzer(forceAnalyzer);
}
queryBuilder.setPhraseSlop(slop);
Query query = queryBuilder.parse(MultiMatchQueryBuilder.Type.PHRASE, fields, queryText, null);
if (query == null) {
return null;
}
return applySlop(query, slop);
} catch (IOException e) {
throw new ParseException(e.getMessage());
} finally {
queryBuilder.setAnalyzer(oldAnalyzer);
queryBuilder.setPhraseSlop(oldSlop);
}
}
这个调研做的比较早了,写这篇文章是后续的梳理,当时梳理,按思路走找到一个可行的点就试,也没有考虑优化,和是否有其他更好的方法
我当时直接为快速验证,直接把QueryStringQueryParser.java内的所有Analyzer 实例都替换成defaultAnalyzer了
现在回头一看,发现了forceAnalyzer,源代码也公开了写入
public void setForceAnalyzer(Analyzer analyzer) {
this.forceAnalyzer = analyzer;
}
// Analyzer normalizer = forceAnalyzer == null ? queryBuilder.context.getSearchAnalyzer(currentFieldType) : forceAnalyzer;
Analyzer normalizer = defaultAnalyzer;
指定forceAnalyzer的话,应该可以不需要通过构造函数传入defaultAnalyzer了,有空做验证
基于源码更改的操作,要尽量作到少改动,改动越少,风险越小,如果验证setForceAnalyzer可行,会作方案变更,指定analyzer的方式不同而已,不影响本文的思路
接下来我们可以操作了,简而言就是
//构造shard信息
QueryShardContext qc = AbstractQueryTestCase.createShardContext();
//构造QueryStringQueryParser类 因为是分词索引
//以es的数据结构,对分词字段,要为分词字段指定分词插件,除了es自带的,中文场景下,会用ik/hanlp/ansj等分词器
QueryParser queryStringQueryParser = new QueryStringQueryParser(qc, "text_field", analyzer);
//执行语法解析
Query query = queryStringQueryParser.parse(esQueryStringPharse);
//实际这个query对象就可以拿来用了,直接应到到lucene上了
大路通顺后,其实还有很多细节没有提到,实际AbstractQueryTestCase.createShardContext()
这里也花了个人较多的时间
部分代码,依然有大量的外部依赖要去除/重载/置为null/重写等,这些较为碎片就不提了,这些也都是风险点,因为精简掉的部分,可能会影响最终的结果
好在还算顺利,操作完成后去掉依赖,验证结果依然保证了准确
以上是提取query对象的部分
es Query应用至Lucene
query对象较为基础,因为query本身就是lucene类QueryParserBase的实例,上方提取出的是QueryParserBase的子类实现,本身就是应用lucene查询而已,可以直接由lucene调用
项目相关代码就不放了,以官方示例说明,后期会开发并公开一些demo性质的,可能大数据相关的示例
官方文档
https://lucene.apache.org/core/8_8_1/index.html
https://lucene.apache.org/core/7_7_3/index.html
索引index
https://lucene.apache.org/core/7_7_3/demo/src-html/org/apache/lucene/demo/IndexFiles.html
lucene下分词字段的类型为TextField
doc.add(new TextField("contents", new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))));
/** Indexes a single document */
static void indexDoc(IndexWriter writer, Path file, long lastModified) throws IOException {
try (InputStream stream = Files.newInputStream(file)) {
Document doc = new Document();
Field pathField = new StringField("path", file.toString(), Field.Store.YES);
doc.add(pathField);
doc.add(new LongPoint("modified", lastModified));
doc.add(new TextField("contents", new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))));
if (writer.getConfig().getOpenMode() == OpenMode.CREATE) {
System.out.println("adding " + file);
writer.addDocument(doc);
} else {
System.out.println("updating " + file);
writer.updateDocument(new Term("path", file.toString()), doc);
}
}
}
索引比较简单不做说明,主要是确定分词和存储方式,大数据应用,用在内存里较多,内存建议索引,直接应用查询,本来也是项目方案的目的,在其他场景下也可以考虑落地或其他文件服务。这个是后话了
检索search
https://lucene.apache.org/core/7_7_3/demo/src-html/org/apache/lucene/demo/SearchFiles.html
public static void main(String[] args) throws Exception {
...
IndexReader reader = DirectoryReader.open(FSDirectory.open(Paths.get(index)));
IndexSearcher searcher = new IndexSearcher(reader);
Analyzer analyzer = new StandardAnalyzer();
BufferedReader in = null;
if (queries != null) {
in = Files.newBufferedReader(Paths.get(queries), StandardCharsets.UTF_8);
} else {
in = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));
}
QueryParser parser = new QueryParser(field, analyzer);
while (true) {
String line = queryString != null ? queryString : in.readLine();
...
Query query = parser.parse(line);
System.out.println("Searching for: " + query.toString(field));
if (repeat > 0) { // repeat & time as benchmark
Date start = new Date();
for (int i = 0; i < repeat; i++) {
searcher.search(query, 100);
}
Date end = new Date();
System.out.println("Time: "+(end.getTime()-start.getTime())+"ms");
}
}
reader.close();
}
searcher.search(query, 100); 部分,这里的query对象,即可使用上方提取出的es生成的对象,当然前提是建索引时的file_name一致
即建立时
doc.add(new TextField("contents", new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))));
查找时
QueryParser queryStringQueryParser = new QueryStringQueryParser(qc, "contents", analyzer);
实际contents会传入应用fields.put(field, 1.0f);
private Map<String, Float> extractMultiFields(String field, boolean quoted) {
Map<String, Float> fields = new HashMap<>();
fields.put(field, 1.0f);
//todo ...
return fields;
}
fields.put(field, 1.0f); 我最早的方案只传入单个key,因此es生成的query也是针对这个key的如,但lucene自定义检索时可能是多key
这个问题有三种方法解决
- 1 这里实现多key
private Map<String, Float> extractMultiFields(String field, boolean quoted) {
Map<String, Float> fields = new HashMap<>();
fields.put("field", 1.0f);
fields.put("field", 1.0f);
//todo ...
return fields;
}
-
2 对每个key分别应用,生成对应的query对象,分别应用search 后并按需取交并 即should/must
-
3 打印出es转换出的string,这里已经转为标准lucene语法了,以该string重新构建query,直接查询 (其实这也是我一开始的目的,但发现其他两种方法更好)
提取出标准lucene方法如下
Query对象,自带toString
方法,如果 fields.put("`filed_name", 1.0f);只传入一项field,那输出的就是针对此单key的查询
打印现来会发现 输出内容会带上filed_name:
前缀
例如
QueryParser queryStringQueryParser = new QueryStringQueryParser(qc, "text_field", analyzer);
输出内容会带上"text_field:"
如果就是要用"text_field:"查询,那直接应用Query即可,如果查询字段不一致,需要重新构造,则需要提取不含"text_field:"的部分,也即替换掉"text_field:"
正则替换即可,考虑性能问题的话,常用的es query_string查询可以预先通过该方法转为lucene 语法,预先保存映射关系
lucenePharse = query.toString().replaceAll("text_field:", "");
lucenePharse即是标准的lucene语法
之后以lucenePharse为参数重新构造一个lucene的多字段Query,应用即可
es-query -> string -> lucene-query,经测试完全结果匹配
隐患
如果"text_field:"是个单词,会因为这种替换丢失,这样导致查询内容和原生es内容不同,查询结果就可能不一致
现在这个方案,最low的地方就是这里"text_field:"的替换,但实际分词的文本中可能会包含"text_field:"这个字符串,因此,最好选择绝不会应用的值
实际业务应用和工作中,"text_field:"是个单词的概率几乎没有,担心影响的话,可以把text_field 换为MD5 ("text_field") = ebfdbe4a280b61b7142a54132d34b993 一类的值,更减少出问题的概率,缺点是正则性能损失一些
这个隐患,这样几乎能作到完全避免了,但还是留意下,如果还觉得有问题,可以换其他方案,直接应用Query对象,这也是更推荐的办法
最终代码如下,因为是以测试用例为入口,部分初始化工作在 c.beforeTest()中,因此看起来会有些奇怪
public static String formatToLucenePhrase(String esQueryStringPharse, Analyzer analyzer) throws Exception {
AbstractQueryTestCase c = new AbstractQueryTestCase();
c.beforeTest();
QueryShardContext qc = AbstractQueryTestCase.createShardContext();
QueryParser queryStringQueryParser = new QueryStringQueryParser(qc, "text_content", analyzer);
Query query = queryStringQueryParser.parse(esQueryStringPharse);
String lucenePharse;
lucenePharse = query.toString().replaceAll("text_context:", "");
System.out.println("es query_string orgnial " + esQueryStringPharse);
System.out.println("es query_string pharse " + query);
System.out.println("es query_string term " + lucenePharse);
return lucenePharse;
}
这只是提取query,提取query文本的部分,需要传入真正实际应用的analyzer才好看到效果,后续会给几个实际应用的demo
提出标准lucene功能的额外应用
我个人验证时主要担心query_string的实际的查询量和lucene的查询量不一致,lucene不能直接用query_string的话法,因为query_string有包装,es是能直接用lucene语法的,因此我验证时的一个方式就是es的query_string 和query_string转换提出lucene的语法,分别查询es,查看最终结果是否一致(另一个验证方法是,从大数hdfs/hbase/hive拉文本,lucene构建,lucene查询,再比较相同数据es索引/检索的数据量)
结果是完全匹配的,不匹配的话,现在的方案就是失败的
因为实际应用的query_string,并不像示例的那样简单,可能有几千,几万词,各种词距离,编辑距离,布尔,嵌套关系等,文本长度甚至超过100k,这样的query_string不可能全是每次独立拼接的,而是各种父子语句结合应用
意外收获是因为产品业务为拼装组合的逻辑生成的query_string不一定是最优的,可能会有重复和失效的部分,但是经过解析和提取后 query_string实际在不损失内容的情况下,大大的优化了,更清晰,文本长度更小,实现了dsl语句的压缩和优化
本来我保存es query_string和 其对应标准lucene语法是为了缓存,减少重复计算。
意外发现精简query_string的查询的用途,长度100k的query string 可以减少一半