打通es及lucene应用,lucene应用es Query,结合非queryString查询(一)

打通es及lucene应用,lucene应用es Query,结合非queryString查询

继上一篇,基本解决queryString字段的应用,在分词字段上打通了es和lucene

结构化数据检索需求

对非queryString字段的搜索如何应用,一开始只是觉得对分词字段的检索是个难点,解决了这个就可以顺利应用了,实际也确实如此

比如hbase可以通过filter过滤列,sql就更不用多说了,对非分词字段所有支持多维结构化数据的数据组件都可以有很好的支持

例如es本身(不做分词),druid,Kylin,clickhouse等

流式spark streaming,flink 分批建立小型索引,再类似es定期小index merge为合并为大index

基本以下几种应用方式

  • 1 通过结构化kv多维过滤一部分数据,拿到所有数据,再对数据内的文本,应用分词,过滤出满足分词查询条件的数据,聚合统计输出结果

  • 2 通过结构化kv多维过滤一部分数据,同时把分词过滤的功能通过插件的方式集成到数据组件中,直接在数据组件内应用分词,聚合统计输出结果(对hive来说是udf)

分词部分由上一篇的结果通过es+lucene来作,其他非分词部分,结构化kv多维检索都通过其他数据组件来作

但是es索引本身也是支持kv多维检索的,是否能直接把分词和非分词字段都都通过lucene索引,一个query同时满足,结构化数据和分词数据的检索?

理论上当然是可以的,es就是这样用,但实际怎么操作,还要调研解决

还是那句,调研路径不同,方式不同,摸着石头过河,最后即使方案可行,但不一定是唯一方案,也可能不是最优的
  • 是否可以联系多类Query 及以何种方式关联

首先我们真正构造query应用的是lucene的类方法MultiFieldQueryParser 该方法有些代码算是个提示

  public MultiFieldQueryParser(String[] fields, Analyzer analyzer) {
    super(null, analyzer);
    this.fields = fields;
  }
  
  @Override
  protected Query getFieldQuery(String field, String queryText, int slop) throws ParseException {
    if (field == null) {
      List<Query> clauses = new ArrayList<>();
      for (int i = 0; i < fields.length; i++) {
        Query q = super.getFieldQuery(fields[i], queryText, true);
        if (q != null) {
          //If the user passes a map of boosts
          if (boosts != null) {
            //Get the boost from the map and apply them
            Float boost = boosts.get(fields[i]);
            if (boost != null) {
              q = new BoostQuery(q, boost.floatValue());
            }
          }
          q = applySlop(q,slop);
          clauses.add(q);
        }
      }
      if (clauses.size() == 0)  // happens for stopwords
        return null;
      return getMultiFieldQuery(clauses);
    }
    Query q = super.getFieldQuery(field, queryText, true);
    q = applySlop(q,slop);
    return q;
  }

  protected Query getMultiFieldQuery(List<Query> queries) throws ParseException {
    if (queries.isEmpty()) {
      return null; // all clause words were filtered away by the analyzer.
    }
    BooleanQuery.Builder query = newBooleanQuery();
    for (Query sub : queries) {
      query.add(sub, BooleanClause.Occur.SHOULD);
    }
    return query.build();
  }

注意到Query是可以通过数据组织,及BooleanClause.Occur.SHOULD,组成数型结构嵌套的,也和使用es的体验一致shoud/must多级嵌套

    BooleanQuery.Builder query = newBooleanQuery();
    for (Query sub : queries) {
      query.add(sub, BooleanClause.Occur.SHOULD);
    }
    return query.build();

我们在之前方案的分词字段Query上,多加一级Query,比如包装为

BooleanQuery [ QueryStringQuery + rangeQuery ] 
  • 以RangeQuery为例,提取生成RangeQuery的 Query对象

我们以比较有代表性的RangeQuery做为例子研究一下,为什么是RangeQuery呢,因为大数据场景不论什么业务,时间是个很重要的过滤条件,出报表也多有时间为维度,时间应用Range查询比较多,因此个人经验上首先想到的就是这个

建立索引时同时索引QueryString字段和range字段,再应用BooleanQuery,理论上就可以达到目的

首先lucene支持大量的原生Query构造,lucene的RangFieldQuery可用,但我们的目标是兼容es的range查询语法,因此目标是使用es的RangFieldQuery

Screen Shot 2021-03-02 at 10.02.04 PM

看es代码,整体上因为之前已经解决过queryString的问题,这次以同样的思路会顺利很多

因为之前解决了queryString(虽然方式不怎么优雅完美),但对这次调研rangeQuery是比较乐观的,不像调研queryString问题时,生怕哪一步被卡住,退后甚至推倒以其他路径重新调研

首先找到RangeQueryBuilder

Screen Shot 2021-02-28 at 9.23.23 AM

QueryStringQueryBuilder 会调用 org.elasticsearch.index.search.QueryStringQueryParser

同样的思路我们找RangeQueryParser,没找到,因为不依赖这个类,扫了眼其他QueryBuilder,如TermQueryBuilder TermsQueryBuilder,也都没有对应的Parser类

QueryStringQueryBuilder毕竟比较特殊,其他的结构化字段都比较简单,因此没有Parser类也正常

我们的目标是Query对象,因此我们找找看RangeQueryBuilder内有没有能返回Query类对象的方法,找到了3个,同时注意到toFilter调用了toQuery

protected Query doToQuery(QueryShardContext context)

public final Query toQuery(QueryShardContext context)

public final Query toFilter(QueryShardContext context)

类本身
public class RangeQueryBuilder extends AbstractQueryBuilder<RangeQueryBuilder> implements MultiTermQueryBuilder
    protected Query doToQuery(QueryShardContext context) //protected,外部不可调用,先记下,如果没其他办法,把源码改为public

抽像父类
public abstract class AbstractQueryBuilder<QB extends AbstractQueryBuilder<QB>> implements QueryBuilder
    public final Query toQuery(QueryShardContext context) throws IOException {
        Query query = this.doToQuery(context);
        if (query != null) {
            if (this.boost != 1.0F) {
                if (query instanceof SpanQuery) {
                    query = new SpanBoostQuery((SpanQuery)query, this.boost);
                } else {
                    query = new BoostQuery((Query)query, this.boost);
                }
            }
            if (this.queryName != null) {
                context.addNamedQuery(this.queryName, (Query)query);
            }
        }
        return (Query)query;
    }

    //可以看到toFilter调用了toQuery  result = this.toQuery(context);我们先用toQuery测试,看看结果,不通再换toFilter
    public final Query toFilter(QueryShardContext context) throws IOException {
        boolean originalIsFilter = context.isFilter();
        Query result;
        try {
            context.setIsFilter(true);
            result = this.toQuery(context);
        } finally {
            context.setIsFilter(originalIsFilter);
        }
        return result;
    }
    protected abstract Query doToQuery(QueryShardContext var1) throws IOException;

有看到这几个方法都需要QueryShardContext实例,我们之前在queryString问题处已经拿到这个实例了,直接拿来用即可

忽然想起个问题,在QueryString问题下,我是找到QueryStringQueryParser,生成Query对象的,而在rangeQuery这里,看来直接通过RangeQueryBuilder就能拿到Query对象,先把rangeQuery解决了,再回头看看QueryString的能否直接通过QueryStringQueryBuilder获取

  • 实例构造Query
    public void rangeQuery() throws Exception {
        AbstractBuilderTestCase c = new AbstractBuilderTestCase();
        c.beforeTest();
        QueryShardContext qc = AbstractBuilderTestCase.createShardContext();
        //实例构造
        RangeQueryBuilder rqb=new RangeQueryBuilder("rangeFieldName");
        rqb.from(1);
        rqb.to(100);
        Query q=rqb.toQuery(qc);
        System.out.println("to Query : "+q);
        Query f=rqb.toFilter(qc);
        System.out.println("to Filter: "+f);
    }

输出

to Query : rangeFieldName:[1 TO 100]
to Filter: rangeFieldName:[1 TO 100]

任务完成,但是新的调研目标需求也来了

  • 加载解析es query 生成Query

实例构造可用,但是如果所有es的查询语句,都通过代码改为实例,那也太费人工了,有没有可以加载es range查询块解析的方式,来研究下

public static RangeQueryBuilder fromXContent(XContentParser parser) throws IOException 这个方法看起来有戏

浏览下代码,发现是json解析类,查询es时也都是提交的json串

其实如果有es java sdk调用经验的一看就知道肯定是这个方法了,不少直接或间接(jpa)基于es client jar包开发,里面也涉及各种Query QueryBuilder,虽然场景不一样,但就json解析和输出,代码结构是差不多的

es为了性能考虑,操作json,并未使用一次性加载整个json串为doc结构,再读取doc字段的方法,而是顺序化的解决

类似解析xml的DOM和SAX方案的区别,一般开发程序比如gson,fastjson多是使用dom,es里json解析类例SAX,并且是实现在代码内的,不引入外部依赖。

SAX这在早期对性能有较高要求(或平台性能较低,如很早期的移动端android/ios开发)的项目里使用


之前很乐观,但这里居然卡住了

因为没找到XContentParser的实例化方法

找到了XContent, JsonXContent,JsonXContentGenerator 几个类,但都不通

之前queryString 只是解析"a and b"这类字符串,而range这里想解析{"range":{"rangeFieldName":{"gt":1,"gt":100}}}这类json格式的串,确实未涉及

路子走到这里不通,我们看看有没有相关测试用例,理论上有

先找到了DateRangeTests

Screen Shot 2021-02-28 at 10.43.25 AM

代码很明显,createParser的参数为

            final HttpGet get = new HttpGet(new URI(str));
            try (
                    CloseableHttpResponse response = client.execute(get);
                    XContentParser parser =
                            JsonXContent.jsonXContent.createParser(
                                    new NamedXContentRegistry(ClusterModule.getNamedXWriteables()),
                                    DeprecationHandler.THROW_UNSUPPORTED_OPERATION,
                                    response.getEntity().getContent())) {
                final Map<String, Object> map = parser.map();
                assertThat(map.get("first_name"), equalTo("John"));
                assertThat(map.get("last_name"), equalTo("Smith"));
                assertThat(map.get("age"), equalTo(25));
                assertThat(map.get("about"), equalTo("I love to go rock climbing"));
                final Object interests = map.get("interests");
                assertThat(interests, instanceOf(List.class));
                @SuppressWarnings("unchecked") final List<String> interestsAsList = (List<String>) interests;
                assertThat(interestsAsList, containsInAnyOrder("sports", "music"));
            }

第一次尝试测试

        final String rangeAggregation = "{\"range\":{\"rangeFieldName\":{\"gt\":\"2021-01-19T00:00:00\"}}}";
        XContentParser parser =
                JsonXContent.jsonXContent.createParser(
                        new NamedXContentRegistry(ClusterModule.getNamedXWriteables()),
                        DeprecationHandler.THROW_UNSUPPORTED_OPERATION,
                        rangeAggregation);
        RangeQueryBuilder rqbFromJson=RangeQueryBuilder.fromXContent(parser);
        rqbFromJson.toQuery(qc);

报错ParsingException[[range] query does not support [range]

然后找到RangeQueryBuilderTests

public void testDateRangeQueryFormat() throws IOException {
        assumeTrue("test runs only when at least a type is registered", getCurrentTypes().length > 0);
        // We test 01/01/2012 from gte and 2030 for lt
        String query = "{\n" +
                "    \"range\" : {\n" +
                "        \"" + DATE_FIELD_NAME + "\" : {\n" +
                "            \"gte\": \"01/01/2012\",\n" +
                "            \"lt\": \"2030\",\n" +
                "            \"format\": \"8dd/MM/yyyy||yyyy\"\n" +
                "        }\n" +
                "    }\n" +
                "}";
        Query parsedQuery = parseQuery(query).toQuery(createShardContext());
        assertThat(parsedQuery, instanceOf(IndexOrDocValuesQuery.class));
        parsedQuery = ((IndexOrDocValuesQuery) parsedQuery).getIndexQuery();
        assertThat(parsedQuery, instanceOf(PointRangeQuery.class));

        assertEquals(LongPoint.newRangeQuery(DATE_FIELD_NAME,
                DateTime.parse("2012-01-01T00:00:00.000+00").getMillis(),
                DateTime.parse("2030-01-01T00:00:00.000+00").getMillis() - 1),
                parsedQuery);

        // Test Invalid format
        final String invalidQuery = "{\n" +
                "    \"range\" : {\n" +
                "        \"" + DATE_FIELD_NAME + "\" : {\n" +
                "            \"gte\": \"01/01/2012\",\n" +
                "            \"lt\": \"2030\",\n" +
                "            \"format\": \"8yyyy\"\n" +
                "        }\n" +
                "    }\n" +
                "}";
        expectThrows(ElasticsearchParseException.class, () -> parseQuery(invalidQuery).toQuery(createShardContext()));
    }

之前一直想作json解析,这里注意到一个方法parseQuery(query),直接入json 字符串即可,另外.toQuery(createShardContext())和之前我们找到的应用方法一致

之前怎么没见到parseQuery 这个方法,定位到

Screen Shot 2021-02-28 at 11.03.36 AM

方法为protected,看起来没有其他依赖,先改为public拿出来试试

protected QueryBuilder parseQuery(AbstractQueryBuilder<?> builder) throws IOException {
    BytesReference bytes = XContentHelper.toXContent(builder, XContentType.JSON, false);
    return parseQuery(createParser(JsonXContent.jsonXContent, bytes));
}

protected QueryBuilder parseQuery(String queryAsString) throws IOException {
    XContentParser parser = createParser(JsonXContent.jsonXContent, queryAsString);
    return parseQuery(parser);
}

protected static QueryBuilder parseQuery(XContentParser parser) throws IOException {
    QueryBuilder parseInnerQueryBuilder = parseInnerQueryBuilder(parser);
    assertNull(parser.nextToken());
    return parseInnerQueryBuilder;
}

这里找到了createParser

Screen Shot 2021-02-28 at 11.24.11 AM

第二次尝试 直接把

org.elasticsearch.index.query.QueryBuilder parseInnerQueryBuilder = RangeQueryBuilder.parseInnerQueryBuilder(parser);
Query rqbFromJson=parseInnerQueryBuilder.toQuery(qc);

报错 ParsingException[no [query] registered for [range] 哪里搞错了

第三次尝试提取了所有依赖,封装后

JsonParser jsonParser=new JsonParser();
org.elasticsearch.index.query.QueryBuilder queryBuilder=jsonParser.parseQuery(query);

依然报错ParsingException[no [query] registered for [range] 哪里搞错了

信心耐心下降很多,目前觉得是解析没问题,但range是不是类似es的mapping需要先registered注册?

解决方向有两条

  • 1 做加法,找到注册位置,注册

  • 2 做减法,找到注册位置,跳过

定位注册位置

官方的RangeQueryBuilderTests 既然测试通过,但必有注册的位置,重点是定位如何

祭出断点大法,看到异常,追踪异常位置,定位到

Screen Shot 2021-02-28 at 11.51.01 AM

https://github.com/elastic/elasticsearch/issues/64602 网络上有相关信息,但匹配度不高,不想搞了-一个原因是基本版本6.8做的,已经是落后的版本,只是总结之前的成功顺带深入下,这块花了太多时间,肚子饿了,可以先放弃

As I understand the issue, the high level client has its XContent registry missing some entries to correctly parse the result of client.transform().getTransform (namely the query field).

不死心,看描述是XContent registry相关,和推测的一致

Screen Shot 2021-02-28 at 12.00.37 PM

这个类果然有注册相关的代码,印象中在QueryShardContext见过这个类,实际在排查过程中,又获得了很多信息,尤其是mapping部分,因为queryString是跳过了mapping,因此总觉得有隐患,想完美解决,必须解决mapping的问题,这个以后有时间再看

public class NamedXContentRegistry {
    public static final NamedXContentRegistry EMPTY = new NamedXContentRegistry(Collections.emptyList());
    private final Map<Class<?>, Map<String, NamedXContentRegistry.Entry>> registry;

    public NamedXContentRegistry(List<NamedXContentRegistry.Entry> entries) {
        if (entries.isEmpty()) {
            this.registry = Collections.emptyMap();
        } else {
            List<NamedXContentRegistry.Entry> entries = new ArrayList(entries);
            entries.sort((e1, e2) -> {
                return e1.categoryClass.getName().compareTo(e2.categoryClass.getName());
            });
    }
}            
    private static class ServiceHolder implements Closeable {

    private final SearchModule searchModule;         
            searchModule = new SearchModule(nodeSettings, false, pluginsService.filterPlugins(SearchPlugin.class));
            IndicesModule indicesModule = new IndicesModule(pluginsService.filterPlugins(MapperPlugin.class));
            List<NamedWriteableRegistry.Entry> entries = new ArrayList<>();
            entries.addAll(indicesModule.getNamedWriteables());
            entries.addAll(searchModule.getNamedWriteables());
            namedWriteableRegistry = new NamedWriteableRegistry(entries);
            xContentRegistry = new NamedXContentRegistry(Stream.of(
                    searchModule.getNamedXContents().stream()
            ).flatMap(Function.identity()).collect(toList()));

找到了注册相关代码,这些信息看起来是ClusterModule相关的 索引信息ClusterModule,找找AbstractModule的其他子类 有没有查询相关的信息

public class ClusterModule extends AbstractModule {
    public static List<org.elasticsearch.common.xcontent.NamedXContentRegistry.Entry> getNamedXWriteables() {
        List<org.elasticsearch.common.xcontent.NamedXContentRegistry.Entry> entries = new ArrayList();
        entries.add(new org.elasticsearch.common.xcontent.NamedXContentRegistry.Entry(org.elasticsearch.cluster.metadata.MetaData.Custom.class, new ParseField("repositories", new String[0]), RepositoriesMetaData::fromXContent));
        entries.add(new org.elasticsearch.common.xcontent.NamedXContentRegistry.Entry(org.elasticsearch.cluster.metadata.MetaData.Custom.class, new ParseField("ingest", new String[0]), IngestMetadata::fromXContent));
        entries.add(new org.elasticsearch.common.xcontent.NamedXContentRegistry.Entry(org.elasticsearch.cluster.metadata.MetaData.Custom.class, new ParseField("stored_scripts", new String[0]), ScriptMetaData::fromXContent));
        entries.add(new org.elasticsearch.common.xcontent.NamedXContentRegistry.Entry(org.elasticsearch.cluster.metadata.MetaData.Custom.class, new ParseField("index-graveyard", new String[0]), IndexGraveyard::fromXContent));
        entries.add(new org.elasticsearch.common.xcontent.NamedXContentRegistry.Entry(org.elasticsearch.cluster.metadata.MetaData.Custom.class, new ParseField("persistent_tasks", new String[0]), PersistentTasksCustomMetaData::fromXContent));
        return entries;
    }
}

Screen Shot 2021-02-28 at 12.32.02 PM

Screen Shot 2021-02-28 at 12.34.03 PM

Screen Shot 2021-02-28 at 12.35.53 PM

找到问题了,并解决了

因为我是从es代码里拷出的,默认会创建一个新的NamedXContentRegistry,这个NamedXContentRegistry,只注册了size=1

改为用QueryShardContext里的NamedXContentRegistry,即解决问题

    protected NamedXContentRegistry xContentRegistry() {
        return new NamedXContentRegistry(ClusterModule.getNamedXWriteables());
    }

RangeQueryBuilder json串解析完成,其他QueryBuilder 同理,应该不会有别的问题了

鉴于这篇花了太多精力,先休息会,下一篇,处理多级queryString和range 这个已经没有技术难点,也不需要看es代码了,只是写代码,验证结果即可

posted @ 2021-03-02 22:12  cclient  阅读(592)  评论(0编辑  收藏  举报