0
点赞
收藏
分享

微信扫一扫

Solr的自动完成/自动补充实现及经验



Solr的自动完成/自动补充实现介绍(第一部分)


大部分人已经见过自动完成(autocomplete)的功能了(见下图),solr提供了构建这个功能的机制。今天,我将给你展示如何使用facet的方式来添加自动完成机制。 



 



索引 


设想你想在你的在线商店中,给用户一些提示,比如商品的名称。假设我们的索引构建如下: 


Xml代码  

1. <field name="id" type="string" indexed="true" stored="true" multiValued="false" required="true"/>
2. <field name="name" type="text" indexed="true" stored="true" multiValued="false" />
3. <field name="description" type="text" indexed="true" stored="true" multiValued="false" />


text类型的定义为: 

Xml代码  


    1. <fieldType name="text" class="solr.TextField" positionIncrementGap="100">
    2. <analyzer>
    3. <tokenizer class="solr.WhitespaceTokenizerFactory"/>
    4. <filter class="solr.WordDelimiterFilterFactory" generateWordParts="1" generateNumberParts="1" catenateWords="1" catenateNumbers="1" catenateAll="0" splitOnCaseChange="1"/>
    5. <filter class="solr.LowerCaseFilterFactory"/>
    6. </analyzer>
    7. </fieldType>

    配置 

    开始前,首先考虑你要实现的功能:是要实现一个名字的提示,还是全名的提示。这都依赖于我们的选择,我们必须为需要引导的地方设置适当的域。 


    单词提示 

    在单词的情况下,我们使用的域也即一个token。在这种情况下,域名为name就足够了。但是,这属于一个词干,analysis的操作都在词干上,因此,我们最好换一个其他的类型。 


    全名提示 

    我们使用一个不同的域配置来定义全名提示--最好一个未被定义的域。但是我们不能使用基于类似string这种类型的域,基于这个原因,我们定义为一下的域: 

    引用


    <field name="name_auto" type="text_auto" indexed="true" stored="true" multiValued="false" />


    text_auto类型的定义为: 

    Xml代码  


    1. <fieldType name="text_auto" class="solr.TextField">
    2. <analyzer>
    3. <tokenizer class="solr.KeywordTokenizerFactory"/>
    4. <filter class="solr.LowerCaseFilterFactory"/>
    5. </analyzer>
    6. </fieldType>



    为了不影响原有数据的格式,将原数据进行拷贝: 

    引用


    <copyField source="name" dest="name_auto" />


    如何使用 

    为了使用这个数据,我们准备了一个简单的查询语句: 

    引用


    需要替换的地方: 

    q=*:*&facet=true&facet.field=FIELD&facet.mincount=1&facet.prefix=USER_QUERY

       FIELD:我们打算提供建议的域,在本例中域名为name 或name_auto 

       USER_QUERY:用户输入的字符 


    这里可以设置rows=0,这样可以只返回facet的结果,而没有查询结果。当然这不是必须的。 


    查询的一个例子可以这样写: 

    引用

    fl=id,name&rows=0&q=*:*&facet=true&facet.field=name_auto&facet.mincount=1&facet.prefix=har


    查询结果会返回这样的结果: 

    Xml代码  

    扩展功能 

    1. <response>
    2. <lst name="responseHeader">
    3. <int name="status">0</int>
    4. <int name="QTime">0</int>
    5. </lst>
    6. <result name="response" numFound="4" start="0"/>
    7. <lst name="facet_counts">
    8. <lst name="facet_queries"/>
    9. <lst name="facet_fields">
    10. <lst name="name_auto">
    11. <int name="hard disk">1</int>
    12. <int name="hard disk samsung">1</int>
    13. <int name="hard disk seagate">1</int>
    14. <int name="hard disk toshiba">1</int>
    15. </lst>
    16. </lst>
    17. <lst name="facet_dates"/></lst>
    18. </response>

    这里说一下他的一些常用的功能。 


    第一个是显示用户的一些额外的信息,比如当你选择某个提示词时,显示的结果的数量。这是一个很有意思的特性。 


    另一个是使用facet.sort参数进行排序。这依赖于你的需求,我们可以按文档的数量排序(默认方式,设参数为true即可),或者按字母序排序(设为false)。 


    我们也可以通过设置facet.mincount来显示比指定的数量更多的提示词。 


    另外一个很好的特性是提示词不仅可以通过用户的类型获取,还可以通过其他的属性获取,这类似于类别。举个例子,我们想给用户展示家庭用品相关的商品,我们假设现在用户对DVD类型的商品并不感兴趣,这样我们添加一个参数: fq=department:homeApplications(假设有这个department)。通过这样的一个查询,你就不需要在所有的索引中匹配了,而是在我们选择的department里选择。 


    结尾 

    跟其他方法一样,它有优点,也有缺点。优点就是易于使用、没有额外的组件依赖,并且能将结果约束在一个很小的范围内来更好的匹配用户的需求;另外一个很大的优点是它对每个提示词都附带了结果的统计。缺点就是需要添加额外的类型和字段;另外由于其facet的机制,对机器性能和load都非常消耗。 



    PS:我自己测试了一下,由于这个功能是实时请求的(每个字母的输入都是一次请求),如果量很大的时候,统计数量会占用很大的内存,内存过小(我的2G)很容易OOM。所以,这个功能慎用。 


    网上有个哥们建议使用facet.prefix,由于目前没有这方面的强烈需求,故在此搁下,需要时再从这里起步。 

     


    Solr的自动完成/自动补充实现介绍(第二部分)

    在Solr的自动完成/自动补充实现介绍(第一部分) 中我介绍了怎么用faceting的机制来实现自动完成(autocomplete)的功能,今天我们来看一下如何用Suggester的组件来实现自动完成功能. 


    开始 

     


    这里有一点需要提醒:Suggest组件在1.4.1或以下版本不可用。要使用这个组件,你需要下载3_x或lucene/solr的主干版本。 


    配置 

    在索引配置之前,我们定义一个searchComponent: 

    Xml代码  

    这个组件是基于 solr.SpellCheckComponent的,这样我们就可以使用它的一些配置。配置中有3个非常重要的属性: 

    1. <searchComponent name="suggest" class="solr.SpellCheckComponent">
    2. <lst name="spellchecker">
    3. <str name="name">suggest</str>
    4. <str name="classname">org.apache.solr.spelling.suggest.Suggester</str>
    5. <str name="lookupImpl">org.apache.solr.spelling.suggest.tst.TSTLookup</str>
    6. <str name="field">name_autocomplete</str>
    7. </lst>
    8. </searchComponent>


    name:组件名 

    lookupImpl:绑定这个搜索的对象,目前有两个类可以使用- JasperLookup、 TSTLookup,第二个效率更高 

    field:针对的字段 


    现在让我们添加合适的handler: 

    Xml代码  


      1. <requestHandler name="/suggest" class="org.apache.solr.handler.component.SearchHandler">
      2. <lst name="defaults">
      3. <str name="spellcheck">true</str>
      4. <str name="spellcheck.dictionary">suggest</str>
      5. <str name="spellcheck.count">10</str>
      6. </lst>
      7. <arr name="components">
      8. <str>suggest</str>
      9. </arr>
      10. </requestHandler>


      非常简单的配置,它定义了Search的组件,告诉solr每次建议的最大个数为10,使用上面定义的suggest组件。 


      索引 

      假设我们的文档有三个字段: id、name、description。我们想给name字段做自动完成功能,索引配置则为: 

      Xml代码  


        1. <field name="id" type="string" indexed="true" stored="true" multiValued="false" required="true"/>
        2. <field name="name" type="text" indexed="true" stored="true" multiValued="false" />
        3. <field name="name_autocomplete" type="text_auto" indexed="true" stored="true" multiValued="false" />
        4. <field name="description" type="text" indexed="true" stored="true" multiValued="false" />



        另外,需要定义一个copyFiled: 

        Xml代码  

        1. <copyField source="name" dest="name_autocomplete" />


        单词建议 

        为了完成单独词的建议,我们需要定义一个 text_autocomplete的类型: 

        Xml代码  


          1. <fieldType class="solr.TextField" name="text_auto" positionIncrementGap="100">
          2. <analyzer>
          3. <tokenizer class="solr.WhitespaceTokenizerFactory"/>
          4. <filter class="solr.WordDelimiterFilterFactory" generateWordParts="1" generateNumberParts="1" catenateWords="1" catenateNumbers="1" catenateAll="0" splitOnCaseChange="1"/>
          5. <filter class="solr.LowerCaseFilterFactory"/>
          6. </analyzer>
          7. </fieldType>



          词组建议 

          如果实现完整的词组建议,我们的text_autocomplete类型应该定义为: 

          Xml代码  


            1. <fieldType class="solr.TextField" name="text_auto">
            2. <analyzer>
            3. <tokenizer class="solr.KeywordTokenizerFactory"/>
            4. <filter class="solr.LowerCaseFilterFactory"/>
            5. </analyzer>
            6. </fieldType>


            如果使用词组,你需要定义自己的转换类(对于中文如庖丁、iK等) 


            建立词典 

            在我们开始使用该组件前,我们需要对它建立索引,可以使用solr命令: 

            Html代码  


              1. /suggest?spellcheck.build=true



              查询 

              现在终于可以使用这个组件了。使用词组的建议方式,假设查询语句为: 

              Html代码  


              1. /suggest?q=har


              执行该语句后,得到下面的建议: 

              Xml代码  

              1. <?xml version="1.0" encoding="UTF-8"?>
              2. <response>
              3. <lst name="responseHeader">
              4. <int name="status">0</int>
              5. <int name="QTime">0</int>
              6. </lst>
              7. <lst name="spellcheck">
              8. <lst name="suggestions">
              9. <lst name="dys">
              10. <int name="numFound">4</int>
              11. <int name="startOffset">0</int>
              12. <int name="endOffset">3</int>
              13. <arr name="suggestion">
              14. <str>hard drive</str>
              15. <str>hard drive samsung</str>
              16. <str>hard drive seagate</str>
              17. <str>hard drive toshiba</str>
              18. </arr>
              19. </lst>
              20. </lst>
              21. </lst>
              22. </response>


              结尾 

              下一部分我将介绍如何修改配置来使用静态的词典信息以及怎么获得更好的建议。该系列的最后一部分将对会这些方法做一个性能的比较,并选出在不同场景下最快的一个。 

               


              Solr的自动完成/自动补充实现介绍(第三部分)

              http://java.dzone.com/news/solr-and-autocomplete-part-3?mz=33057-solr_lucene 

              在之前的两个部分(part1、part2)中,我们学会了如何配置和查询solr来获取自动完成的功能。今天,我们来看一下如果为suggester添加字段,以这种方式来提供自动完成的功能。 

               


              组件配置 

              在上一期的配置组件中添加如下的参数: 

              Xml代码  


                1. <str name="sourceLocation">dict.txt</str>


                这样我们的配置就变成了: 

                Java代码  

                1. <searchComponent name="suggest" class="solr.SpellCheckComponent">   
                2. <lst name="spellchecker">   
                3. <str name="name">suggest</str>   
                4. <str name="classname">org.apache.solr.spelling.suggest.Suggester</str>    
                5. <str name="lookupImpl">org.apache.solr.spelling.suggest.tst.TSTLookup</str>   
                6. <str name="field">name_autocomplete</str>   
                7. <str name="sourceLocation">dict.txt</str>   
                8. </lst>   
                9. </searchComponent>

                使用这个参数,我们让suggest组件使用名叫dict.txt的文件作为solr的配置字典。 


                handler配置 

                handler的配置也需要添加额外的一个参数: 

                Xml代码  


                  1. <str name="spellcheck.onlyMorePopular">true</str>



                  完整的配置为: 

                  Xml代码  


                  1. <requestHandler name="/suggest" class="org.apache.solr.handler.component.SearchComponent">
                  2. <lst name="defaults">
                  3. <str name="spellcheck">true</str>
                  4. <str name="spellcheck.dictionary">suggest</str>
                  5. <str name="spellcheck.count">10</str>
                  6. <str name="spellcheck.onlyMorePopular">true</str>
                  7. </lst>
                  8. <arr name="components">
                  9. <str>suggest</str>
                  10. </arr>
                  11. </requestHandler>


                  这个参数告诉solr,当查询的结果数多于设定的count数时,返回点击数更多的那些。 


                  Dictionary 

                  我们告诉solr来使用这个字段,那么这个字段长的什么样呢?下面来看一个例子: 

                  引用



                  # sample dict      
                  Hard disk hitachi      
                  Hard disk wd    2.0      
                  Hard disk jjdd    3.0

                  这个字典的结果是什么样的呢?每个词组放在单独的一行中,每行以改词组的权重为结束(权重与词组之间以TAB字符分隔),这个权重就是跟 spellcheck.onlyMorePopular=true 香港的参数,默认值为1.0。该字段必须以UTF-8的编码格式存储。每行前有#字符的将被忽略(注释行)。 


                  数据 

                  以这种方式,我们不需要数据,字段就是数据。 


                  运行 

                  在重新构建suggester之后,我们来看一下它的运行情况,输入命令: 

                  引用


                  /suggest?q=Har


                  得到的结果为: 

                  Xml代码  

                  结束语 

                  1. <?xml version="1.0" encoding="UTF-8"?>
                  2. <response>
                  3. <lst name="responseHeader">
                  4. <int name="status">0</int>
                  5. <int name="QTime">0</int>
                  6. </lst>
                  7. <lst name="spellcheck">
                  8. <lst name="suggestions">
                  9. <lst name="Dys">
                  10. <int name="numFound">3</int>
                  11. <int name="startOffset">0</int>
                  12. <int name="endOffset">3</int>
                  13. <arr name="suggestion">
                  14. <str>Hard disk jjdd</str>
                  15. <str>Hard disk wd</str>
                  16. <str>Hard disk hitachi</str>
                  17. </arr>
                  18. </lst>
                  19. </lst>
                  20. </lst>
                  21. </response>

                  跟预期一样,suggest的结果是按权重排序的。这里的大小写敏感(注意首字母).  Solr本身的性能不错,但是在使用过程中,还是会遇到一些使用错误,或是没考虑到的地方;在出现瓶颈时,可以首先考虑哪些点呢?下面就来看一下Solr官方的总结,个人觉得总结的很好。SOLR+LUCENE的官网还是挺给力的 

                   


                  对Schema设计的考虑

                   

                  索引域的数量增长会很大程度的影响以下的内容: 

                   

                  引用


                  索引期间的内存使用 
                  段的合并时间 
                  优化(optimization)时间 


                  如果设置omitNorms="true" ,则可以减小对这些影响 

                  批注:如果设置Norms,则会影响评分的标准,但会大大的增大索引文件的大小,如果对该字段没有需求,建议关掉  



                  存储域

                   

                  通过查询结果获取存储域的值是一个相当大的开销。如果文档的数据特别大,或者一些数据存储到了分布式的磁盘中(需要更多的IO来查询域)时,那么花费将会很大。这在存储大数据时很容易被考虑到,尤其是整个文档内容的存储。 


                  考虑将大数据的存储放到solr之外。如果非要这么做,那么可以考虑使用压缩域,这将会用CPU的开销来换取IO的开销。 


                  如果你并不需要使用所有的存储域,允许延迟加载(enableLazyFieldLoading)将会是很好的方式,由于是对那些压缩的字段。 


                  批注:延迟加载在查询期间很有用,尤其是需要对某些字段作额外的处理时,它既能减少内存使用,又加速了程序的处理。另外,尽量减小索引的大小绝对不是坏事。  


                  SOLR配置考虑

                   


                  mergeFactor

                   


                  mergeFactor大致决定了段的数量。mergeFactor的值告诉lucene有多少个段需要进行合并。它可以被认为是一个基本的数量系统。 


                  举个例子,如果你设置mergeFactor为10,每1000个文档时会创建一个新的段到硬盘中。当第10个段被添加时,所有的10个段将被合并为1个段 (包含10000个文档);当这样的10个文档被创建时,它们又会被合并为个包含100,000个文档的段,依次类推(当然也有上限)。这样,在任何时候,都不会有多余9个的段(相同索引大小情况下)存在。 


                  该值在solrconfig.xml中的mainIndex设置(它会忽略indexDefaults)。 

                  批注:关于合并的策略,请看我之前的博客:lucene内部的合并策略  


                  mergeFactor Tradeoffs

                   

                  高值的merge factor(比如25): 

                  引用


                  Pro:一般会加快索引的速度 
                  Con:低合并延迟,在查询时需要搜索更多的文件,所以会使查询变慢



                  低值的merge factor(比如2): 

                  引用


                  Pro:更少的索引文件,加快查询的速度 
                  Con:更多的文件合并,将使索引变慢



                  批注:一般来说不需要这么极端,设10即可。保证读速度的同时,也保证合并的速度。  


                  HashDocSet最大值的考虑

                   

                  SOLR1.4之后不支持了,不再描述。

                   


                  cache中autoWarm数量的考虑

                   

                  当一个新的searcher被打开时,它的cache可以从旧的searcher中重新加载或者自动预热(autowarmd)缓存的对象。autowarmCount是将被拷贝到新searcher中的对象的数量,你需要根据autowarm的时间来设置autowarmCount。如何使用autowarmCount,需要你根据时间和数量来设定。 


                  批注:autoWarm即新的searcher会有多少数据被缓存,如果没有缓存,一些热点数据无疑会变得很慢。所以,合理的这是这个值,能大大加快查询的效率。  



                  缓存命中率

                   

                  在Solr的admin中监控缓存的统计。增加缓存的大小通常是提高性能的最好方法,尤其是你对一个指定的缓存类型作逐出操作时。请关注filterCache,它也被用来作solr的facetting。 


                  批注:一个典型的场景是范围查询,类似fl=price:[100 TO 200]这样的情况,将数据该范围存储起来时,对其他的一些查询都可以复用这个缓存的数据,很高效。  


                  对排序的域作明确的预热

                   

                  如果你的工作大多基于排序的方式,那么你最好在“newSearcher”和“firstSearcher”时间监听器中添加明确的预热查询规则,这样FiledCache可以在用户的查询被执行前就将数据加载。 


                  优化的考虑

                   

                  你可能想在任何时候都可以优化你的索引。比如你创建索引后,就没有修改过它。 


                  如果你的索引收到了一串需要更新的流,那么请考虑以下的因素: 

                  引用



                  1. 如果过多的段被添加到索引中,那么查询的性能将会下降;lucene的段自动合并能将段的数量控制在一定范围 
                  2. auto-warming的时间也会延长,它通常依赖于所做的查询 
                  3. 优化后的第一次分布耗时比之后的分布耗时要长。具体请看 Collection Distribution 
                  4. 在优化期间索引的问题大小会加倍,优化后会回到原始大小或更小 
                  5. 如果可以,请确保没有并发的commit请求,否则会有很大的性能损失 



                  在优化时所有的索引会放到唯一的段中;优化索引会避免“文件打开数过多”的问题。 

                  这里有一篇关于该问题的文章:ONJava Article  


                  更新和提交的频率

                   

                  如果slaves收到的数据过频,那么性能必然受损。为了避免这个问题,你必须了解slaver的更新机制,这样你才能更好的调整相关的参数(commit的数量/频率、snappullers、autowarming/autocount)以使新数据的写入不会那么频繁。 


                  引用


                  1. 集合的快照会在客户端运行commit时建立,或者在optimization时;这依赖于在master上的postCommit或postOptimize的钩子方法 
                  2. slaver上的Snappuller会运行corn去检查master上是否有新的快照,如果它找到新的版本,就会把它拿过来并install这些新的数据。 
                  3. 当一个新的searcher被打开时,autowarming会先于Solr的查询请求之前完成。有了预热的缓存,查询的延迟将会小很多。 



                  这里有三个相关的参数: 

                  引用


                  快照的数量/频率:这取决于客户端的索引。因此,集合的版本号依赖于客户端的活跃度 
                  snappluller:基于cron,他可以精确到秒级别。它们运行时,会获取最近它们没有的集合 
                  缓存预热:在solrconfig.xml中配置



                  查询响应的压缩

                   

                  在Solr返回xml应答给客户端之前对其进行压缩有时是值得做的。如果应答结果非常大,或者网络IO有限制,或者没有千兆网卡,请考虑使用压缩机制。 


                  压缩会增加CPU的使用,并且Solr本身也是CPU密集型的应用,所以压缩会降低查询的性能。压缩会使文件减小到1/6的大小,使网络包减小到1/3的大小;相对的,查询的性能会降低15%左右。 


                  请查看你的应用服务器的相关文档(tomcat、resion、jetty...)来获取关于压缩的信息。 


                  索引的性能

                   

                  一般情况下,一次更新多个文档比一个一个更新要快。 


                  对于这种块级的更新方式,考虑使用 StreamingUpdateSolrServer.java ,它提供多线程多连接的方式来更新流数据。 

                  批注:StreamingUpdateSolrServer类相对CommonsHttpSolrServer要快很多,主要在于它将原本单个的文档写入变为了批量写入,加上多线程多连接的方式,性能上快了超多。我们的测试数据表明,至少要快4-6倍以上。  


                  内存使用的考虑

                   


                  OutOfMemoryErrors

                   


                  如果你的solr实例没有足够的内存,那么JVM有时会抛出OutOfMemoryErrors。这并不会对数据有影响,并且solr也会试图优美的恢复它。任何 添加/删除/提交 的命令在异常抛出时都可能不成功;其他不利的影响也可能会产生。对应用而言,如果SimpleFSLock 的锁机制在使用的话,OutOfMemoryError 会导致solr丢失这个锁。如果这发生了,更新索引的结果将会是这样的异常: 

                  Java代码  

                  1. SEVERE: Exception during commit/optimize:java.io.IOException: Lock obtain timed out: SimpleFSLock@/tmp/lucene-5d12dd782520964674beb001c4877b36-write.lock


                  如果你想在OOM时看堆的情况,请设置"-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/the/dump" 


                  JVM内存的分配

                   

                  针对这个错误的最简单方法,在JVM并没有完全使用你的物理内存时,考虑加大JVM的内存容量: 

                  Java代码  


                    1. java -Xms512M -Xmx1024M -jar start.jar

                    影响内存使用的因素

                     


                    你可能想去减小solr的内存使用。 


                    一个方式就是减小文档的大小。 


                    当运行add命令时,标准的xml更新请求会有两个限制: 


                    引用


                    1. 所有的文档必须同时放入到内存中。通常,它的取值为sum(域的实际长度,maxFieldLength)。所以,调整maxFieldLength的大小可能会有帮助 
                    2. 每个<field>...</field>标签都必须放入到内存中,而不管maxFieldLength



                    注意一些不同的add请求会在不同的线程中并发运行。越多的线程,就会导致越多的内存使用。 



                    我的一些其他使用经验: 

                    1.schema中的类型定义很重要,它直接影响了索引的性能 

                    2.尽量少用filter,虽然它很好用,但是其hashSet的数量如果过多,很容易oom 

                    3. cache的类,都用FastLRUCache吧,LRUCache还有锁,太慢了 

                    4. 通过docId取doc的过程看似平常,但是量大了就是一个灾难,在这点需要根据实际场景考虑 

                    5. 能用缓存的用缓存,不能用缓存的,尝试使用MMapDirectoryFactory,最好是SSD硬盘

                     

                    6.其他,待想到了再补充

                    你有什么建议呢?如果我们有一个很好的字典,这个字典的权重是基于用户的查询行为产生的,那么用户肯定会喜欢它!如果没有好的字典,还是不要用这种方式的好。 

                     


                    微信公众号:


                    Solr的自动完成/自动补充实现及经验_自动完成

                    举报

                    相关推荐

                    0 条评论