您现在的位置是:首页 > 技术文章 > 详情<<文章列表阅读 搜索引擎入门——聊聊schema.xml配置 Java 搜索引擎 schema 分词器 wjyuian 2019-07-24 737 1 ### 前言 Lucene中一个很重要的概念就是文档(Document),它代表一条建立索引的独立且完整的数据。可以对标到我们关系数据库的一条记录。一个Document包含很多个域(Field),对标数据库的字段Column。Field的一些属性配置对标字段的属性。 本身Lucene对Document的Field是开放式的,不同Field的Document可以索引到一起,有点类似于noSQL的概念,属于schema-free的。但是这种开放式结构会造成“开发一时爽,维护骂爹娘”的情况,所以Solr在封装Lucene的时候通过`schema.xml`文件来规范Document的Field定义。类似于MongoDB的一些ORM框架(Morephia、spring-data-mongo)做的事,其实就是定义一个标准、做个存根,方便排查。 所以,`schema.xml`配置的内容就出来了: + Field Type 定义:定义了字段类型,string、int、double、text等等,名字是自取的。 + Field 定义:字段,比如position_id、name、age等等。 ------ ### schema.xml的大致结构 ```XML position_id keyword ``` 从实际的配置文件也可以发现,schema中确实主要包含两个内容,types和fields。 + types:定义Field类型的元数据。 + fields:表示这个索引的每条数据包含哪些字段,分别是什么类型。 + copyField:表示将哪些字段(source)的值复制到某个字段(dest)。 > 比如我们的职位索引包含了职位标题、职位描述、任职要求、职位公司、工作地点等字段,如果想要关键字搜索包含这些字段,那么需要每个字段都拼接查询条件,然后通过OR连接。这样显得比较麻烦。 > 那么,我们可以建立一个名为`keyword`的综合字段,将以上需要关键字搜索的所有字段通过`StringBuilder`拼接好,然后设置到`keyword`中。 > 通过字段拼接虽然灵活,但是麻烦;这时候就用到copyField了。我们配置多个的copyField,source分别是那些字段,dest就是`keyword`。 + similarity:配置了似度类,这个类必须是继承了`lucene-core.4.7.2.jar`中的`org.apache.lucene.search.similarities.Similarity`。 + uniqueKey:定义了索引的主键,类似mySQL的唯一主键。 + defaultSearchField:查询参数q默认的查询域,当用户输入`q=java`时,默认搜索的就是这个参数指定的字段。当然用户可以通过`q=title:java`来指定查询域为`title`。 + solrQueryParser:指定Solr参数q的默认查询逻辑。当用户输入`q=title:java age:[25 TO 30] city:上海`,此时有三个查询条件,且用户未指定查询逻辑,那么就按照这里配置的默认的`AND`逻辑。如果要全量设置查询逻辑关键字,可以通过参数`q.op=OR`来指定,也可以直接拼接在查询语句中`q=title:java AND age:[25 TO 30] AND city:上海`。 ### types定义 ```XML ``` #### name 就是一个唯一标识符,在后面fields定义时通过属性`type`指定。 #### class 定义此类型实际的行为类。这个属性的值都是以`solr.`开头的,例如上面`solr.StrField`。solr对应标准的Solr目录`org.apache.solr.schema`。关于`StrField`、`solr.BoolField`等实际类型,可以去源码查看定义和解释。 `StrField`字段是不会进行分词的,只会索引和存储。 #### precisionStep(很重要) 对于`TrieIntField, TrieFloatField, TrieLongField, TrieDoubleField`这些继承与`TrieField`的数值类型字段,所指定的一个精度值,这也是一个数字,默认值是8。它主要是用于数值字段的范围查询,前提是Field的indexed属性必须是true。 在`TrieField.java`的初始化方法中可以看到,其默认值是`precisionStepArg`,为8。如果配置值是小于等于0或者大于等于64,则配置值会重置为`Integer.MAX_VALUE`。相当于没有启用这一配置,此时该字段的范围查询将会很慢,与普通的分词查询一样。 ```Java @Override protected void init(IndexSchema schema, Map args) { super.init(schema, args); String p = args.remove("precisionStep"); if (p != null) { precisionStepArg = Integer.parseInt(p); } // normalize the precisionStep precisionStep = precisionStepArg; if (precisionStep<=0 || precisionStep>=64) precisionStep=Integer.MAX_VALUE; String t = args.remove("type"); if (t != null) { try { type = TrieTypes.valueOf(t.toUpperCase(Locale.ROOT)); } catch (IllegalArgumentException e) { throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Invalid type specified in schema.xml for field: " + args.get("name"), e); } } } ``` 在`TrieField`中使用的`NumericRangeQuery`对该参数有比较详细的介绍: > 该值越小,在索引阶段产生的token将会越多,索引的大小也会越大,但是会提升范围查询性能。这里建议的合理值范围是1~8,一般可以设置为4。一个字段值在索引是产生的token数公式是`indexedTermsPerValue = ceil(bitsPerValue / precisionStep)`。 > 合适的precisionStep值通常由具体数据类型和使用场景决定的: + 当未指定时,所有数值类型的precisionStep默认值都是4(TrieField中设置了默认值是8)。 + 64位数据类型,比如long、double,理想的precisionStep值是6或者8。 + 32位数据类型,比如int、float,理想的precisionStep值是4。 + 如果某个字段的数值基数比较小,那么将precisionStep设置大一些会比较好。比如,数值都小于100,可以直接选择`Integer.MAX_VALUE`作为precisionStep值。 + 由上面那个公式可以看出来,precisionStep值大于等于数据类型位数时(long、double类型,precisionStep>=64;int、float类型,precisionStep>=32),每个字段值在索引时都只会产生一个token,因此在查询的时候会非常慢,与普通的`TermRangeQuery`差不多。 + 如果你的数值字段只要求排序,不要求范围查询,那么通过将其配置为0或者大于64的任意int值即可。这样设置时,虽然查询效率几乎和纯文本字段一致,但是它的索引效率和排序效率却是远高于纯文本(`StrField`)类型的数字字段(将数字存储到文本字段)。 > Comparisons of the different types of RangeQueries on an index with about 500,000 docs showed that TermRangeQuery in boolean rewrite mode (with raised BooleanQuery clause count) took about 30-40 secs to complete, TermRangeQuery in constant score filter rewrite mode took 5 secs and executing this class took <100ms to complete (on an Opteron64 machine, Java 1.5, 8 bit precision step). This query type was developed for a geographic portal, where the performance for e.g. bounding boxes or exact date/time stamps is important. #### sortMissingLast、sortMissingFirst 这两个是可选属性,由命名可知其作用是在排序时处理那些缺失排序字段的文档。 假设我们的排序字段是`age`, - sortMissingLast="true":所有缺失`age`字段的文档将会排在含有`age`字段的文档后面。 - sortMissingFirst="true":所有缺失`age`字段的文档将会排在含有`age`字段的文档前面。 - 默认情况下,sortMissingLast="false"且sortMissingFirst="false":Lucene将会采用其默认的处理方式,如果是`age`升序,缺失`age`的文档将会排在最前面;如果是降序,缺失`age`的文档将会排在最后。可以这么理解,缺失`age`字段的文档,其`age`字段有默认值且永远都是最小值。 #### indexed 设置这个type的默认indexed属性,可以被field覆盖。ture表示会进行索引,false表示不进行索引,也就是无法通过该字段进行查询、排序。默认值是false。 #### stored 设置这个type的默认stored属性,可以被field覆盖。true表示存储,可以通过查询结果返回该字段值,false表示该字段不会存储。通常大文本字段的stored设置为false,indexed设置为true,表示只索引不存储,可以通过查询该字段来得到文档主键,却不会存储大量文本内容,节省空间。默认值是false。 #### multiValued 设置这个type的默认multiValued属性,可以被field覆盖。true表示这个类型的字段支持多值,false表示单值。 **注:schema.version大于等于1.1,type上的multiValued设置无效** ------ ### 分词类型 solr.TextField 这也是属于types定义的一种,只是它的定义方式与前面介绍的那些不分词字段差别比较大,而且比较重要,故而单独拿出来进行讲解。先给我我们项目中的配置,然后进行一一说明: ```XML ``` #### solr.TextField 看过前面的介绍就不会找错这个类,因为`TextField`类有很多,Lucene源码中也有`org.apache.lucene.document.TextField`。但这里配置的是`org.apache.solr.schema.TextField`。 与上面的其它type一样,它也是通过`org.apache.solr.schema.FieldTypePluginLoader.create(SolrResourceLoader, String, String, Node)`方法来进行FieldType初始化的。 ```Java @Override protected FieldType create( SolrResourceLoader loader, String name, String className, Node node ) throws Exception { // 普通类型,通过类加载器来初始对象实例 FieldType ft = loader.newInstance(className, FieldType.class); ft.setTypeName(name); // 加载查询阶段使用的分词器,type=query // 注:1 String expression = "./analyzer[@type='query']"; Node anode = (Node)xpath.evaluate(expression, node, XPathConstants.NODE); Analyzer queryAnalyzer = readAnalyzer(anode); // 加载多term分词器:模糊查询、正则匹配、占位符查询等 expression = "./analyzer[@type='multiterm']"; anode = (Node)xpath.evaluate(expression, node, XPathConstants.NODE); Analyzer multiAnalyzer = readAnalyzer(anode); // 指定type=index或者未指定type的,都是索引阶段使用的分词器 // 注:2 expression = "./analyzer[not(@type)] | ./analyzer[@type='index']"; anode = (Node)xpath.evaluate(expression, node, XPathConstants.NODE); Analyzer analyzer = readAnalyzer(anode); // 加载用户指定的相似度计算类 // 注:3 expression = "./similarity"; anode = (Node)xpath.evaluate(expression, node, XPathConstants.NODE); //这是一个solr封装的工厂类,里面持有了lucene定义的Similarity类对象 SimilarityFactory simFactory = IndexSchema.readSimilarity(loader, anode); if (null != simFactory) { ft.setSimilarity(simFactory); } // 如果为定义query阶段的分词器,则用index阶段的分词器代替,并设置,query分词器是否明确定义为false if (null == queryAnalyzer) { queryAnalyzer = analyzer; ft.setIsExplicitQueryAnalyzer(false); } else { ft.setIsExplicitQueryAnalyzer(true); } // 如果index阶段分词器未定义,则用query阶段分词器代替,也可能两者都为空 if (null == analyzer) { analyzer = queryAnalyzer; ft.setIsExplicitAnalyzer(false); } else { ft.setIsExplicitAnalyzer(true); } // index、query两个阶段,至少定义了一个分词器,则进入下面的赋值流程 if (null != analyzer) { ft.setAnalyzer(analyzer); ft.setQueryAnalyzer(queryAnalyzer); if (ft instanceof TextField) { // 这里的text对应类型是TextField if (null == multiAnalyzer) { // 如果未定义多term分词器,则用query分词器代替 multiAnalyzer = constructMultiTermAnalyzer(queryAnalyzer); ((TextField)ft).setIsExplicitMultiTermAnalyzer(false); } else { ((TextField)ft).setIsExplicitMultiTermAnalyzer(true); } ((TextField)ft).setMultiTermAnalyzer(multiAnalyzer); } } if (ft instanceof SchemaAware){ schemaAware.add((SchemaAware) ft); } return ft; } ``` **注1:** 通过``来指定query(查询)阶段所使用的分词器,并进行相关配置。 **注2:** 通过``或者``来指定index(所以)阶段所使用的分词器,并进行相关配置。 **注3:** 这里的相似度类定义与上面介绍的配置在最外层的`similarity`标签一样。``可以配置在``里面,也可以配置在``里面。 `class`可以配置两种类型: 1. `org.apache.lucene.search.similarities.Similarity`的子类;初始化之后会将其通过`org.apache.solr.schema.SimilarityFactory`匿名类的方式进行包装。 2. `org.apache.solr.schema.SimilarityFactory`的子类,要实现`public abstract Similarity getSimilarity()`方法,次方法返回的还是`org.apache.lucene.search.similarities.Similarity`的子类。 ```Java static SimilarityFactory readSimilarity(SolrResourceLoader loader, Node node) { if (node==null) { return null; } else { SimilarityFactory similarityFactory; final String classArg = ((Element) node).getAttribute(SimilarityFactory.CLASS_NAME); final Object obj = loader.newInstance(classArg, Object.class, "search.similarities."); if (obj instanceof SimilarityFactory) { final NamedList namedList = DOMUtil.childNodesToNamedList(node); namedList.add(SimilarityFactory.CLASS_NAME, classArg); SolrParams params = SolrParams.toSolrParams(namedList); similarityFactory = (SimilarityFactory)obj; similarityFactory.init(params); } else { // 这里会有类型强转的风险 similarityFactory = new SimilarityFactory() { @Override public Similarity getSimilarity() { return (Similarity) obj; } }; } return similarityFactory; } } ``` #### analyzer.index 我在Solr中使用的分词器是`IK`分词器,所以index和query阶段的配置方式都是基于`IK`的配置。 ```XML ``` ``指定的class值是IK分词器实现类的完整路径,它继承了`org.apache.lucene.analysis.util.TokenizerFactory`。我在index阶段设置了`useSmart=false`,这是为了在索引阶段分出更多、更全面的关键词,主要是提高查全率。不过查准率会有所影响,因为出现了很多歧义分词。 被注释掉的那一个filter是自己实现的,用于过滤单字切词的。我默认认为,单个字是不存在全文意义的,类似于停用词中的`你`、`的`,对与这些关键字的搜索,我直接返回无结果,因为无法分词,所以不会存在单字索引。但是招聘行业也有单字是有意义的,比如`C`表示C语言,所以这个filter中做了动态例外。 后面三个filter是lucene预定义的,其作用分别是过滤停用词、转小写、关键字标记。 关于分词器扩展以及filter链扩展,包括同义词扩展,可以参考我的另一篇文章[《搜索引擎进阶——IK分词器扩展》](https://oomabc.com/articledetail?atclid=e738d22188194d3fac7577d3c38a2219) #### analyzer.query ```XML ``` 在query阶段,分词器配置与index阶段很相似,主要有处不同: 1. useSmart设置为true,目的是尽量保持用户关键字的原意,所以采用了智能分词,其实大致就是最大分词。 2. 增加了两个filter。同义词过滤转换`org.wltea.analyzer.lucene.IKSynonymFilterFactory`和重复关键字过滤`solr.RemoveDuplicatesTokenFilterFactory`。 关于分词设置`useSmart`的区别,可以参考另一篇文章[《搜索引擎开发——关键字预处理模块》](https://oomabc.com/articledetail?atclid=cecf525bb9184a10a7f22ef619c4544f)的前言模块。 ------ ### fields 这一部分定义了当前索引Core的文档结构,也就是包含哪些字段。需要注意的是,从Solr4.+之后的版本,必须要包含至少两个field定义: 1. 主键字段:也就是在uniqueKey标签定义过的字段,例子中的是``。 2. `_version_`字段:``。这是Solr自己会赋值的字段。 #### name 指定字段名,正常情况下的命名规范是由小写英文、下划线、数字组成。英文开头,多个单词用下划线分隔。 #### type 这就是在types中定义的所有type,值就是type的name。 #### indexed 当前字段是否索引。false表示字段不会进行索引;true表示会进行索引,可以在该字段上进行筛选、排序。如果未配置,会从对应type定义上继承该配置值,默认值是false。 #### stored 当前字段是否存储。 #### multiValued 是否是多值字段。 #### required 是否是必填字段。true表示必填,类似mySQL的NOT NULL。 #### dynamicField 动态字段,字段名可以动态设置。一般来说,我会预留一些动态字段,以应对将来的字段新增。索引一旦超过10G,每次修改schema并重启会很费时,而且有一定风险。所以针对添加新字段的修改,通常通过预留的动态字段来实现。预留字段的原则是,每个类型各留一个单值动态字段、多值动态字段。 ```XML ``` 关于`schema.xml`的相关配置就聊到这了。 ------ 相关文章 搜索引擎进阶——IK扩展之动态加载与同义词 SpringBoot2从零开始(二)——多数据源配置 应用算法学习(一)—— TopN算法 重温Java设计模式——工厂模式 重温Java设计模式——建造者模式 Nginx的nginx.conf配置部分解释 从零开发参数同步框架(六)—— 简版配置中心 Java网络编程之Netty框架学习(一) Java网络编程之Netty学习(二)—— 简单RPC实现 Java网络编程之Netty学习(三)—— RPC的服务注册、发现、降级 栏目导航 关于我 不止技术 工程化应用(23) 技术学习/探索(32) 自娱自乐(2) 还有生活 随便写写(1) 娱乐/放松(1) 点击排行 SpringBoot2从零开始(二)——多数据源配置 搜索引擎进阶——IK扩展之动态加载与同义词 从零开发参数同步框架(二)—— 前期准备之工具类 Nginx的nginx.conf配置部分解释 springMVC中controller参数拦截问题处理 Maven项目一键打包、上传、重启服务器 微信小程序深入踩坑总结 微信小程序的搜索高亮、自定义导航条等踩坑记录 标签云 Java(19) 搜索引擎(13) Solr(7) 参数同步(6) SpringBoot(4) ES(3) ElasticSearch(3) JVM(3) Netty(3) Spring(3) mongoDB(3) 设计模式(3) Curator(2) Docker(2) Dubbo(2) 大家推荐 魔神重返战场!厄祭战争的巴巴托斯:第四形态 搜索引擎入门——Solr查询参数详解以及如何使用Java完成对接 来聊一聊这个被淘汰的图片验证码 搜索引擎入门——聊聊schema.xml配置 搜索引擎入门——启动第一个Solr应用 君子性非异也,善假于物也——功能强大的Postman 择其善而从之——我为什么开始学习ElasticSearch 实现一个关于队列的伪需求是一种怎样的体验