小菜毛毛技術分享

          與大家共同成長

            BlogJava :: 首頁 :: 聯系 :: 聚合  :: 管理
            164 Posts :: 141 Stories :: 94 Comments :: 0 Trackbacks

          輕松為應用程序構建搜索和索引功能

          developerWorks
          文檔選項
          將打印機的版面設置成橫向打印模式

          打印本頁

          將此頁作為電子郵件發送

          將此頁作為電子郵件發送

          樣例代碼

          英文原文

          英文原文


          級別: 初級

          Amol Sonawane, 資深軟件工程師, IBM

          2009 年 9 月 14 日

          本文將探討 Apache Lucene —— 性能卓越、功能全面的文本搜索引擎庫。我們將學習 Lucene 架構及其核心 API。學習如何使用 Lucene 進行跨平臺全文本搜索、建立索引、顯示結果,以及如何擴展搜索。

          簡介

          Lucene 是一個開源、高度可擴展的搜索引擎庫,可以從 Apache Software Foundation 獲取。您可以將 Lucene 用于商業和開源應用程序。Lucene 強大的 API 主要關注文本索引和搜索。它可以用于為各種應用程序構建搜索功能,比如電子郵件客戶端、郵件列表、Web 搜索、數據庫搜索等等。Wikipedia、TheServerSide、jGuru 和 LinkedIn 等網站都使用了 Lucene。

          Lucene 還為 Eclipse IDE、Nutch(著名的開源 Web 搜索引擎)以及 IBM®、AOL 和 Hewlett-Packard 等公司提供搜索功能。Lucene 已經兼容許多其他編程語言,包括 Perl、Python、C++ 和 .NET。到 2009 年 7 月 30 日止,用于 Java™ 編程語言的最新版 Lucene 為 V2.4.1。

          Lucene 功能眾多:

          • 擁有強大、準確、有效的搜索算法。
          • 計算每個文檔匹配給定查詢的分數,并根據分數返回最相關的文檔。
          • 支持許多強大的查詢類型,比如 PhraseQuery、WildcardQuery、RangeQuery、FuzzyQuery、BooleanQuery 等。
          • 支持解析人們輸入的豐富查詢表達式。
          • 允許用戶使用定制排序、過濾和查詢表達式解析擴展搜索行為。
          • 使用基于文件的鎖定機制保護并發索引修改。
          • 允許同時搜索和編制索引。




          回頁首


          使用 Lucene 構建應用程序

          如圖 1 所示,使用 Lucene 構建功能全面的搜索應用程序主要涉及編制數據索引、搜索數據和顯示搜索結果幾個方面。


          圖 1. 使用 Lucene 構建應用程序的步驟
          使用 Lucene 構建應用程序的步驟

          本文從使用 Lucene V2.4.1 和 Java 技術開發的樣例應用程序中挑選了一些代碼片段。示例應用程序為存儲在屬性文件中一組電子郵件文檔編制索引,并展示了如何使用 Lucene 的查詢 API 搜索索引。該示例還讓您熟悉基本的索引操作。





          回頁首


          為數據編制索引

          Lucene 允許您為任何文本格式的數據編制索引。Lucene 可以用于幾乎任何數據源以及從中提取的文本信息。您可以使用 Lucene 編制索引并搜索 HTML 文檔、Microsoft® Word 文檔、PDF 文件中存儲的數據。編制數據索引的第一步是讓數據變成一個簡單的文本格式。您可以使用定制解析器和數據轉換器實現這一點。

          編制索引的過程

          編制索引 是將文本數據轉換為有利于快速搜索的格式。這類似于書本后面的索引:為您指出主題在書中出現的位置。

          Lucene 將輸入數據存儲在名為逆序 索引的數據結構中, 該數據結構以索引文件集的形式存儲在文件系統或內存中。大部分 Web 搜索引擎都使用逆序索引。它允許用戶執行快速關鍵字查詢,查找匹配給定查詢的文檔。在將文本數據添加到索引前,由分析程序(使用分析過程)進行處理。

          分析

          分析 是將文本數據轉換為搜索基本單位(稱為項(term))的過程。在分析過程中,文本數據將經歷多項操作:提取單詞、移除通用單詞、忽略標點符號、將單詞變為詞根形式、將單詞變成小寫等等。分析過程發生在編制索引和查詢解析之前。分析將文本數據轉換為標記,這些標記將作為項添加到 Lucene 索引中。

          Lucene 有多種內置分析程序,比如 SimpleAnalyzer、StandardAnalyzer、StopAnalyzer、SnowballAnalyzer 等。它們在標記文本和應用過濾器的方式上有所區別。因為分析在編制索引之前移除單詞,它減少了索引的大小,但是不利用精確的查詢過程。您可以使用 Lucene 提供的基本構建塊創建定制分析程序,以自己的方式控制分析過程。表 1 展示了一些內置分析程序及其處理數據的方式。


          表 1. Lucene 的內置分析程序
          分析程序 對文本數據的操作
          WhitespaceAnalyzer 分解空白處的標記
          SimpleAnalyzer 分解非字母字符的文本,并將文本轉為小寫形式
          StopAnalyzer 移除虛字(stop word)—— 對檢索無用的字,并將文本轉為小寫形式
          StandardAnalyzer 根據一種復雜語法(識別電子郵件地址、縮寫、中文、日文、韓文字符、字母數字等等)標記文本
          將文本轉為小寫形式
          移除虛字

          核心索引編制類

          Directory
          表示索引文件存儲位置的抽象類。有兩個常用的子類:
          • FSDirectory — 在實際文件系統中存儲索引的 Directory 實現。該類對于大型索引非常有用。
          • RAMDirectory — 在內存中存儲所有索引的實現。該類適用于較小的索引,可以完整加載到內存中,在應用程序終止之后銷毀。由于索引保存在內存中,所以速度相對較快。
          Analyzer
          正如上文所述,分析程序負責處理文本數據并將其轉換為標記存儲在索引中。在編制索引前,IndexWriter 接收用于標記數據的分析程序。要為文本編制索引,您應該使用適用于該文本語言的分析程序。

          默認分析程序適用于英語。在 Lucene 沙盒中還有其他分析程序,包括用于中文、日文和韓文的分析程序。

          IndexDeletionPolicy
          該接口用來實現從索引目錄中定制刪除過時提交的策略。默認刪除策略是 KeepOnlyLastCommitDeletionPolicy,該策略僅保留最近的提交,并在完成一些提交之后立即移除所有之前的提交。

           

          IndexWriter
          創建或維護索引的類。它的構造函數接收布爾值,確定是否創建新索引,或者打開現有索引。它提供在索引中添加、刪除和更新文檔的方法。

          對索引所做的更改最初緩存在內存中,并周期性轉儲到索引目錄。IndexWriter 公開了幾個控制如何在內存中緩存索引并寫入磁盤的字段。對索引的更改對于 IndexReader 不可見,除非調用 IndexWriter 的提交或關閉方法。IndexWriter 創建一個目錄鎖定文件,以通過同步索引更新保護索引不受破壞。IndexWriter 允許用戶指定可選索引刪除策略。


          列表 1. 使用 Lucene IndexWriter
                                  //Create instance of Directory where index files will be stored
                                  Directory fsDirectory =  FSDirectory.getDirectory(indexDirectory);
                                  /* Create instance of analyzer, which will be used to tokenize
                                  the input data */
                                  Analyzer standardAnalyzer = new StandardAnalyzer();
                                  //Create a new index
                                  boolean create = true;
                                  //Create the instance of deletion policy
                                  IndexDeletionPolicy deletionPolicy = new KeepOnlyLastCommitDeletionPolicy();
                                  indexWriter =new IndexWriter(fsDirectory,standardAnalyzer,create,
                                  deletionPolicy,IndexWriter.MaxFieldLength.UNLIMITED);
                                  

          將數據添加到索引

          將文本數據添加到索引涉及到兩個類。

          Field 表示搜索中查詢或檢索的數據片。Field 類封裝一個字段名稱及其值。Lucene 提供了一些選項來指定字段是否需要編制索引或分析,以及值是否需要存儲。這些選項可以在創建字段實例時傳遞。下表展示了 Field 元數據選項的詳細信息。


          表 2. Field 元數據選項的詳細信息
          選項 描述
          Field.Store.Yes 用于存儲字段值。適用于顯示搜索結果的字段 — 例如,文件路徑和 URL。
          Field.Store.No 沒有存儲字段值 — 例如,電子郵件消息正文。
          Field.Index.No 適用于未搜索的字段 — 僅用于存儲字段,比如文件路徑。
          Field.Index.ANALYZED 用于字段索引和分析 — 例如,電子郵件消息正文和標題。
          Field.Index.NOT_ANALYZED 用于編制索引但不分析的字段。它在整體中保留字段的原值 — 例如,日期和個人名稱。

          Document 是一個字段集合。Lucene 也支持推進文檔和字段,這在給某些索引數據賦予重要性時非常有用。給文本文件編制索引包括將文本數據封裝在字段中、創建文檔、填充字段,使用 IndexWriter 向索引添加文檔。

          列表 2 展示向索引添加數據的示例。


          列表 2. 向索引添加數據
                                  /*Step 1. Prepare the data for indexing. Extract the data. */
                                  String sender = properties.getProperty("sender");
                                  String date = properties.getProperty("date");
                                  String subject = properties.getProperty("subject");
                                  String message = properties.getProperty("message");
                                  String emaildoc = file.getAbsolutePath();
                                  /* Step 2. Wrap the data in the Fields and add them to a Document */
                                  Field senderField =
                                  new Field("sender",sender,Field.Store.YES,Field.Index.NOT_ANALYZED);
                                  Field emaildatefield =
                                  new Field("date",date,Field.Store.NO,Field.Index.NOT_ANALYZED);
                                  Field subjectField =
                                  new Field("subject",subject,Field.Store.YES,Field.Index.ANALYZED);
                                  Field messagefield =
                                  new Field("message",message,Field.Store.NO,Field.Index.ANALYZED);
                                  Field emailDocField =
                                  new Field("emailDoc",emaildoc,Field.Store.YES,
                                  Field.Index.NO);
                                  Document doc = new Document();
                                  // Add these fields to a Lucene Document
                                  doc.add(senderField);
                                  doc.add(emaildatefield);
                                  doc.add(subjectField);
                                  doc.add(messagefield);
                                  doc.add(emailDocField);
                                  //Step 3: Add this document to Lucene Index.
                                  indexWriter.addDocument(doc);
                                  





          回頁首


          搜索索引數據

          搜索是在索引中查找單詞并查找包含這些單詞的文檔的過程。使用 Lucene 的搜索 API 構建的搜索功能非常簡單明了。本小節討論 Lucene 搜索 API 的主要類。

          Searcher

          Searcher 是一個抽象基類,包含各種超負荷搜索方法。IndexSearcher 是一個常用的子類,允許在給定的目錄中存儲搜索索引。Search 方法返回一個根據計算分數排序的文檔集合。Lucene 為每個匹配給定查詢的文檔計算分數。IndexSearcher 是線程安全的;一個實例可以供多個線程并發使用。

          Term

          Term 是搜索的基本單位。它由兩部分組成:單詞文本和出現該文本的字段的名稱。Term 對象也涉及索引編制,但是可以在 Lucene 內部創建。

          Query 和子類

          Query 是一個用于查詢的抽象基類。搜索指定單詞或詞組涉及到在項中包裝它們,將項添加到查詢對象,將查詢對象傳遞到 IndexSearcher 的搜索方法。

          Lucene 包含各種類型的具體查詢實現,比如 TermQuery、BooleanQuery、PhraseQuery、PrefixQuery、RangeQuery、MultiTermQuery、FilteredQuery、SpanQuery 等。以下部分討論 Lucene 查詢 API 的主查詢類。

          TermQuery
          搜索索引最基本的查詢類型。可以使用單個項構建 TermQuery。項值應該區分大小寫,但也并非全是如此。注意,傳遞的搜索項應該與文檔分析得到的項一致,因為分析程序在構建索引之前對原文本執行許多操作。

          例如,考慮電子郵件標題 “Job openings for Java Professionals at Bangalore”。假設您使用 StandardAnalyzer 編制索引。現在如果我們使用 TermQuery 搜索 “Java”,它不會返回任何內容,因為本文本應該已經規范化,并通過 StandardAnalyzer 轉成小寫。如果搜索小寫單詞 “java”,它將返回所有標題字段中包含該單詞的郵件。


          列表 3. 使用 TermQuery 搜索
                                  //Search mails having the word "java" in the subject field
                                  Searcher indexSearcher = new IndexSearcher(indexDirectory);
                                  Term term = new Term("subject","java");
                                  Query termQuery = new TermQuery(term);
                                  TopDocs topDocs = indexSearcher.search(termQuery,10);
                                  

          RangeQuery
          您可以使用 RangeQuery 在某個范圍內搜索。索引中的所有項都以字典順序排列。Lucene 的 RangeQuery 允許用戶在某個范圍內搜索項。該范圍可以使用起始項和最終項(包含兩端或不包含兩端均可)指定。


          列表 4. 在某個范圍內搜索
                                  /* RangeQuery example:Search mails from 01/06/2009 to 6/06/2009
                                  both inclusive */
                                  Term begin = new Term("date","20090601");
                                  Term end = new Term("date","20090606");
                                  Query query = new RangeQuery(begin, end, true);
                                  

          PrefixQuery
          您可以使用 PrefixQuery 通過前綴單詞進行搜索,該方法用于構建一個查詢,該查詢查找包含以指定單詞前綴開始的詞匯的文檔。


          列表 5. 使用 PrefixQuery 搜索
                                  //Search mails having sender field prefixed by the word 'job'
                                  PrefixQuery prefixQuery = new PrefixQuery(new Term("sender","job"));
                                  PrefixQuery query = new PrefixQuery(new Term("sender","job"));
                                  

          BooleanQuery
          您可以使用 BooleanQuery 組合任何數量的查詢對象,構建強大的查詢。它使用 query 和一個關聯查詢的子句,指示查詢是應該發生、必須發生還是不得發生。在 BooleanQuery 中,子句的最大數量默認限制為 1,024。您可以調用 setMaxClauseCount 方法設置最大子句數。


          列表 6. 使用 BooleanQuery 進行搜索
                                  // Search mails have both 'java' and 'bangalore' in the subject field
                                  Query query1 = new TermQuery(new Term("subject","java"));
                                  Query query2 = new TermQuery(new Term("subject","bangalore"));
                                  BooleanQuery query = new BooleanQuery();
                                  query.add(query1,BooleanClause.Occur.MUST);
                                  query.add(query2,BooleanClause.Occur.MUST);
                                  

          PhraseQuery
          您可以使用 PhraseQuery 進行短語搜索。PhraseQuery 匹配包含特定單詞序列的文檔。PhraseQuery 使用索引中存儲的項的位置信息。考慮匹配的項之間的距離稱為 slop。默認情況下,slop 的值為零,這可以通過調用 setSlop 方法進行設置。PhraseQuery 還支持多個項短語。


          列表 7. 使用 PhraseQuery 進行搜索
                                  /* PhraseQuery example: Search mails that have phrase 'job opening j2ee'
                                  in the subject field.*/
                                  PhraseQuery query = new PhraseQuery();
                                  query.setSlop(1);
                                  query.add(new Term("subject","job"));
                                  query.add(new Term("subject","opening"));
                                  query.add(new Term("subject","j2ee"));
                                  

          WildcardQuery
          WildcardQuery 實現通配符搜索查詢,這允許您搜索 arch*(可以查找包含 architect、architecture 等)之類的單詞。使用兩個標準通配符:
          • * 表示零個以上
          • ? 表示一個以上
          如果使用以通配符查詢開始的模式進行搜索,則可能會引起性能的降低,因為這需要查詢索引中的所有項以查找匹配文檔。


          列表 8. 使用 WildcardQuery 進行搜索
                                  //Search for 'arch*' to find e-mail messages that have word 'architect' in the subject
                                  field./
                                  Query query = new WildcardQuery(new Term("subject","arch*"));
                                  

          FuzzyQuery
          您可以使用 FuzzyQuery 搜索類似項,該類匹配類似于指定單詞的單詞。類似度測量基于 Levenshtein(編輯距離)算法進行。在列表 9 中,FuzzyQuery 用于查找與拼錯的單詞 “admnistrtor” 最接近的項,盡管這個錯誤單詞沒有索引。


          列表 9. 使用 FuzzyQuery 進行搜索
                                  /* Search for emails that have word similar to 'admnistrtor' in the
                                  subject field. Note we have misspelled admnistrtor here.*/
                                  Query query = new FuzzyQuery(new Term("subject", "admnistrtor"));
                                  

          QueryParser
          QueryParser 對于解析人工輸入的查詢字符非常有用。您可以使用它將用戶輸入的查詢表達式解析為 Lucene 查詢對象,這些對象可以傳遞到 IndexSearcher 的搜索方法。它可以解析豐富的查詢表達式。 QueryParser 內部將人們輸入的查詢字符串轉換為一個具體的查詢子類。您需要使用反斜杠(\)將 *? 等特殊字符進行轉義。您可以使用運算符 ANDORNOT 構建文本布爾值查詢。


          列表 10. 搜索人工輸入的查詢表達式
          QueryParser queryParser = new QueryParser("subject",new StandardAnalyzer());
                                  // Search for emails that contain the words 'job openings' and '.net' and 'pune'
                                  Query query = queryParser.parse("job openings AND .net AND pune");
                                  





          回頁首


          顯示搜索結果

          IndexSearcher 返回一組對分級搜索結果(如匹配給定查詢的文檔)的引用。您可以使用 IndexSearcher 的搜索方法確定需要檢索的最優先搜索結果數量。可以在此基礎上構建定制分頁。您可以添加定制 Web 應用程序或桌面應用程序來顯示搜索結果。檢索搜索結果涉及的主要類包括 ScoreDocTopDocs

          ScoreDoc
          搜索結果中包含一個指向文檔的簡單指針。這可以封裝文檔索引中文檔的位置以及 Lucene 計算的分數。
          TopDocs
          封裝搜索結果以及 ScoreDoc 的總數。

          以下代碼片段展示了如何檢索搜索結果中包含的文檔。


          列表 11. 展示搜索結果
          /* First parameter is the query to be executed and
                                  second parameter indicates the no of search results to fetch */
                                  TopDocs topDocs = indexSearcher.search(query,20);
                                  System.out.println("Total hits "+topDocs.totalHits);
                                  // Get an array of references to matched documents
                                  ScoreDoc[] scoreDosArray = topDocs.scoreDocs;
                                  for(ScoreDoc scoredoc: scoreDosArray){
                                  //Retrieve the matched document and show relevant details
                                  Document doc = indexSearcher.doc(scoredoc.doc);
                                  System.out.println("\nSender: "+doc.getField("sender").stringValue());
                                  System.out.println("Subject: "+doc.getField("subject").stringValue());
                                  System.out.println("Email file location: "
                                  +doc.getField("emailDoc").stringValue());
                                  }
                                  

          基本的索引操作

          基本的索引操作包括移除和提升文檔。

          從索引中移除文檔

          應用程序常常需要使用最新的數據更新索引并移除較舊的數據。例如,在 Web 搜索引擎中,索引需要定期更新,因為總是需要添加新網頁,移除不存在的網頁。Lucene 提供了 IndexReader 接口允許您對索引執行這些操作。

          IndexReader 是一個提供各種方法訪問索引的抽象類。Lucene 內部引用文檔時使用文檔編號,該編號可以在向索引添加或從中移除文檔時更改。文檔編號用于訪問索引中的文檔。IndexReader 不得用于更新目錄中的索引,因為已經打開了 IndexWriterIndexReader 在打開時總是搜索索引的快照。對索引的任何更改都可以看到,直到再次打開 IndexReader。使用 Lucene 重新打開它們的 IndexReader 可以看到最新的索引更新。


          列表 12. 從索引中刪除文檔
          // Delete all the mails from the index received in May 2009.
                                  IndexReader indexReader = IndexReader.open(indexDirectory);
                                  indexReader.deleteDocuments(new Term("month","05"));
                                  //close associate index files and save deletions to disk
                                  indexReader.close();
                                  

          提升文檔和字段

          有時您需要給某些索引數據更高的重要級別。您可以通過設置文檔或字段的提升因子實現這一點。默認情況下,所有文檔和字段的默認提升因子都是 1.0。


          列表 13. 提升字段
          if(subject.toLowerCase().indexOf("pune") != -1){
                                  // Display search results that contain pune in their subject first by setting boost factor
                                  subjectField.setBoost(2.2F);
                                  }
                                  //Display search results that contain 'job' in their sender email address
                                  if(sender.toLowerCase().indexOf("job")!=-1){
                                  luceneDocument.setBoost(2.1F);
                                  }
                                  





          回頁首


          擴展搜索

          Lucene 提供一個稱為排序 的高級功能。您可以根據指示文檔在索引中相對位置的字段對搜索結果進行排序。用于排序的字段必須編制索引但不得標記。搜索字段中可以放入 4 種可能的項值:整數值、long 值、浮點值和字符串。

          還可以通過索引順序排序搜索結果。Lucene 通過降低相關度(比如默認的計算分數)對結果排序。排序的順序是可以更改的。


          列表 14. 排序搜索結果
          /* Search mails having the word 'job' in subject and return results
                                  sorted by sender's email in descending order.
                                  */
                                  SortField sortField = new SortField("sender", true);
                                  Sort sortBySender = new Sort(sortField);
                                  WildcardQuery query = new WildcardQuery(new Term("subject","job*"));
                                  TopFieldDocs topFieldDocs =
                                  indexSearcher.search(query,null,20,sortBySender);
                                  //Sorting by index order
                                  topFieldDocs = indexSearcher.search(query,null,20,Sort.INDEXORDER);
                                  

          Filtering 是限制搜索空間,只允許某個文檔子集作為搜索范圍的過程。您可以使用該功能實現對搜索結果進行再次搜索,或者在搜索結果上實現安全性。Lucene 帶有各種內置的過濾器,比如 BooleanFilter、CachingWrapperFilter、ChainedFilter、DuplicateFilter、PrefixFilter、QueryWrapperFilter、RangeFilter、RemoteCachingWrapperFilter、SpanFilter 等。Filter 可以傳遞到 IndexSearcher 的搜索方法,以過濾匹配篩選標準的篩選文檔。


          列表 15. 篩選搜索結果
          /*Filter the results to show only mails that have sender field
                                  prefixed with 'jobs' */
                                  Term prefix = new Term("sender","jobs");
                                  Filter prefixFilter = new PrefixFilter(prefix);
                                  WildcardQuery query = new WildcardQuery(new Term("subject","job*"));
                                  indexSearcher.search(query,prefixFilter,20);
                                  





          回頁首


          結束語

          Lucene 是來自 Apache 的一個非常流行的開源搜索庫, 它為應用程序提供了強大的索引編制和搜索功能。它提供了一個簡單易用的 API,只需要稍微了解索引編制和搜索的原理即可使用。在本文中,您學習了 Lucene 架構及其核心 API。

          Lucene 為許多知名網站和組織提供了各種強大的搜索功能。它還兼容許多其他編程語言。Lucene 有一個活躍的大型技術用戶社區。如果您需要一些易用、可擴展以及高性能的開源搜索庫,Apache Lucene 是一個極佳的選擇。






          回頁首


          下載

          描述 名字 大小 下載方法
          Lucene 代碼示例 os-apache-lucenesearch-SampleApplication.zip 755KB HTTP
          關于下載方法的信息


          參考資料

          學習
          • 了解所有有關 Apache Lucene 的內容,包括最新新聞。

          • Lucene in Action(作者:Erik Hatcher 和 Otis Gospodnetic)是 Lucene 的權威指南。它描述了如何編制數據索引,包括您必須了解的幾種類型,比如 MS Word、PDF、HTML 和 XML。它介紹了如何搜索、排序、過濾和高亮顯示搜索結果。

          • 要收聽面向軟件開發人員的有趣訪談和討論,請查看 developerWorks 播客

          • 隨時關注 developerWorks 技術活動網絡廣播

          • 查閱最近將在全球舉辦的面向 IBM 開放源碼開發人員的研討會、交易展覽、網絡廣播和其他 活動

          • 訪問 developerWorks 開放源碼專區,獲得豐富的 how-to 信息、工具和項目更新,幫助您用開放源碼技術進行開發,并與 IBM 產品結合使用。

          • 查看免費的 developerWorks 演示中心,觀看并了解 IBM 及開源技術和產品功能。


          獲得產品和技術

          討論
          posted on 2009-09-17 19:45 小菜毛毛 閱讀(352) 評論(0)  編輯  收藏 所屬分類: 搜索引擎
          主站蜘蛛池模板: 新余市| 长泰县| 岳阳县| 潢川县| 南涧| 衡水市| 丰原市| 兰坪| 封丘县| 蒲城县| 甘肃省| 永吉县| 秭归县| 凤山县| 十堰市| 高雄市| 巍山| 全州县| 堆龙德庆县| 长沙市| 孝昌县| 兴宁市| 枣阳市| 四平市| 独山县| 青阳县| 大港区| 湛江市| 巩义市| 高碑店市| 巩留县| 宜黄县| 尼玛县| 淮安市| 汉寿县| 枣阳市| 旬阳县| 华宁县| 墨脱县| 岗巴县| 唐海县|