久久精品国产精品国产精品污,男人扒开添女人下部免费视频,一级国产69式性姿势免费视频,夜鲁夜鲁很鲁在线视频 视频,欧美丰满少妇一区二区三区,国产偷国产偷亚洲高清人乐享,中文 在线 日韩 亚洲 欧美,熟妇人妻无乱码中文字幕真矢织江,一区二区三区人妻制服国产

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Lucene学习总结之四:Lucene索引过程分析

發布時間:2024/1/23 编程问答 18 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Lucene学习总结之四:Lucene索引过程分析 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.


對于Lucene的索引過程,除了將詞(Term)寫入倒排表并最終寫入Lucene的索引文件外,還包括分詞(Analyzer)和合并段(merge segments)的過程,本次不包括這兩部分,將在以后的文章中進行分析。

Lucene的索引過程,很多的博客,文章都有介紹,推薦大家上網搜一篇文章:《Annotated Lucene》,好像中文名稱叫《Lucene源碼剖析》是很不錯的。

想要真正了解Lucene索引文件過程,最好的辦法是跟進代碼調試,對著文章看代碼,這樣不但能夠最詳細準確的掌握索引過程(描述都是有偏差的,而代碼是不會騙你的),而且還能夠學習Lucene的一些優秀的實現,能夠在以后的工作中為我所用,畢竟Lucene是比較優秀的開源項目之一。

由于Lucene已經升級到3.0.0了,本索引過程為Lucene 3.0.0的索引過程。

一、索引過程體系結構

Lucene 3.0的搜索要經歷一個十分復雜的過程,各種信息分散在不同的對象中分析,處理,寫入,為了支持多線程,每個線程都創建了一系列類似結構的對象集,為了提高效率,要復用一些對象集,這使得索引過程更加復雜。

其實索引過程,就是經歷下圖中所示的索引鏈的過程,索引鏈中的每個節點,負責索引文檔的不同部分的信息 ,當經歷完所有的索引鏈的時候,文檔就處理完畢了。最初的索引鏈,我們稱之基本索引鏈?。

為了支持多線程,使得多個線程能夠并發處理文檔,因而每個線程都要建立自己的索引鏈體系,使得每個線程能夠獨立工作,在基本索引鏈基礎上建立起來的每個線程獨立的索引鏈體系,我們稱之線程索引鏈?。線程索引鏈的每個節點是由基本索引鏈中的相應的節點調用函數addThreads創建的。

為了提高效率,考慮到對相同域的處理有相似的過程,應用的緩存也大致相當,因而不必每個線程在處理每一篇文檔的時候都重新創建一系列對象,而是復用這些對象。所以對每個域也建立了自己的索引鏈體系,我們稱之域索引鏈?。域索引鏈的每個節點是由線程索引鏈中的相應的節點調用addFields創建的。

當完成對文檔的處理后,各部分信息都要寫到索引文件中,寫入索引文件的過程是同步的,不是多線程的,也是沿著基本索引鏈將各部分信息依次寫入索引文件的。

下面詳細分析這一過程。

?

二、詳細索引過程

1、創建IndexWriter對象

代碼:

IndexWriter writer = new IndexWriter(FSDirectory.open(INDEX_DIR), new StandardAnalyzer(Version.LUCENE_CURRENT), true, IndexWriter.MaxFieldLength.LIMITED);

IndexWriter對象主要包含以下幾方面的信息:

  • 用于索引文檔
    • Directory directory;? 指向索引文件夾
    • Analyzer analyzer;??? 分詞器
    • Similarity similarity = Similarity.getDefault(); 影響打分的標準化因子(normalization factor)部分,對文檔的打分分兩個部分,一部分是索引階段計算的,與查詢語句無關,一部分是搜索階段計算的,與查詢語句相關。
    • SegmentInfos segmentInfos = new SegmentInfos(); 保存段信息,大家會發現,和segments_N中的信息幾乎一一對應。
    • IndexFileDeleter deleter; 此對象不是用來刪除文檔的,而是用來管理索引文件的。
    • Lock writeLock; 每一個索引文件夾只能打開一個IndexWriter,所以需要鎖。
    • Set segmentsToOptimize = new HashSet(); 保存正在最優化(optimize)的段信息。當調用optimize的時候,當前所有的段信息加入此Set,此后新生成的段并不參與此次最優化。
  • 用于合并段,在合并段的文章中將詳細描述
    • SegmentInfos localRollbackSegmentInfos;
    • HashSet mergingSegments = new HashSet();
    • MergePolicy mergePolicy = new LogByteSizeMergePolicy(this);
    • MergeScheduler mergeScheduler = new ConcurrentMergeScheduler();
    • LinkedList pendingMerges = new LinkedList();
    • Set runningMerges = new HashSet();
    • List mergeExceptions = new ArrayList();
    • long mergeGen;
  • 為保持索引完整性,一致性和事務性
    • SegmentInfos rollbackSegmentInfos; 當IndexWriter對索引進行了添加,刪除文檔操作后,可以調用commit將修改提交到文件中去,也可以調用rollback取消從上次commit到此時的修改。
    • SegmentInfos localRollbackSegmentInfos; 此段信息主要用于將其他的索引文件夾合并到此索引文件夾的時候,為防止合并到一半出錯可回滾所保存的原來的段信息。?
  • 一些配置
    • long writeLockTimeout; 獲得鎖的時間超時。當超時的時候,說明此索引文件夾已經被另一個IndexWriter打開了。
    • int termIndexInterval; 同tii和tis文件中的indexInterval。

?

有關SegmentInfos對象所保存的信息:

  • 當索引文件夾如下的時候,SegmentInfos對象如下表


segmentInfos??? SegmentInfos? (id=37)????
??? capacityIncrement??? 0????
??? counter??? 3????
??? elementCount??? 3????
??? elementData??? Object[10]? (id=68)????
??????? [0]??? SegmentInfo? (id=166)????
??????????? delCount??? 0????
??????????? delGen??? -1????
??????????? diagnostics??? HashMap? (id=170)????
??????????? dir??? SimpleFSDirectory? (id=171)????
??????????? docCount??? 2????
??????????? docStoreIsCompoundFile??? false????
??????????? docStoreOffset??? -1????
??????????? docStoreSegment??? null????
??????????? files??? ArrayList? (id=173)????
??????????? hasProx??? true????
??????????? hasSingleNormFile??? true????
??????????? isCompoundFile??? 1????
??????????? name??? "_0"????
??????????? normGen??? null????
??????????? preLockless??? false????
??????????? sizeInBytes??? 635????
??????? [1]??? SegmentInfo? (id=168)????
??????????? delCount??? 0????
??????????? delGen??? -1????
??????????? diagnostics??? HashMap? (id=177)????
??????????? dir??? SimpleFSDirectory? (id=171)????
??????????? docCount??? 2????
??????????? docStoreIsCompoundFile??? false????
??????????? docStoreOffset??? -1????
??????????? docStoreSegment??? null????
??????????? files??? ArrayList? (id=178)????
??????????? hasProx??? true????
??????????? hasSingleNormFile??? true????
??????????? isCompoundFile??? 1????
??????????? name??? "_1"????
??????????? normGen??? null????
??????????? preLockless??? false????
??????????? sizeInBytes??? 635????
??????? [2]??? SegmentInfo? (id=169)????
??????????? delCount??? 0????
??????????? delGen??? -1????
??????????? diagnostics??? HashMap? (id=180)????
??????????? dir??? SimpleFSDirectory? (id=171)????
??????????? docCount??? 2????
??????????? docStoreIsCompoundFile??? false????
??????????? docStoreOffset??? -1????
??????????? docStoreSegment??? null????
??????????? files??? ArrayList? (id=214)????
??????????? hasProx??? true????
??????????? hasSingleNormFile??? true????
??????????? isCompoundFile??? 1????
??????????? name??? "_2"????
??????????? normGen??? null????
??????????? preLockless??? false????
??????????? sizeInBytes??? 635?????
??? generation??? 4????
??? lastGeneration??? 4????
??? modCount??? 3????
??? pendingSegnOutput??? null????
??? userData??? HashMap? (id=146)????
??? version??? 1263044890832???

有關IndexFileDeleter:

  • 其不是用來刪除文檔的,而是用來管理索引文件的。
  • 在對文檔的添加,刪除,對段的合并的處理過程中,會生成很多新的文件,并需要刪除老的文件,因而需要管理。
  • 然而要被刪除的文件又可能在被用,因而要保存一個引用計數,僅僅當引用計數為零的時候,才執行刪除。
  • 下面這個例子能很好的說明IndexFileDeleter如何對文件引用計數并進行添加和刪除的。

(1) 創建IndexWriter時????

IndexWriter writer = new IndexWriter(FSDirectory.open(indexDir), new StandardAnalyzer(Version.LUCENE_CURRENT), true, IndexWriter.MaxFieldLength.LIMITED);?
writer.setMergeFactor(3);

索引文件夾如下:


引用計數如下:

refCounts??? HashMap? (id=101)?????
??? size??? 1????
??? table??? HashMap$Entry[16]? (id=105)?????
??????? [8]??? HashMap$Entry? (id=110)?????
??????????? key??? "segments_1"?????
??????????? value??? IndexFileDeleter$RefCount? (id=38)????
??????????????? count??? 1

(2) 添加第一個段時

indexDocs(writer, docDir);?
writer.commit();

首先生成的不是compound文件


因而引用計數如下:

refCounts??? HashMap? (id=101)?????
??? size??? 9????
??? table??? HashMap$Entry[16]? (id=105)?????
??????? [1]??? HashMap$Entry? (id=129)?????
??????????? key??? "_0.tis"?????
??????????? value??? IndexFileDeleter$RefCount? (id=138)????
??????????????? count??? 1?????
??????? [3]??? HashMap$Entry? (id=130)?????
??????????? key??? "_0.fnm"?????
??????????? value??? IndexFileDeleter$RefCount? (id=141)????
??????????????? count??? 1?????
??????? [4]??? HashMap$Entry? (id=134)?????
??????????? key??? "_0.tii"?????
??????????? value??? IndexFileDeleter$RefCount? (id=142)????
??????????????? count??? 1?????
??????? [8]??? HashMap$Entry? (id=135)?????
??????????? key??? "_0.frq"?????
??????????? value??? IndexFileDeleter$RefCount? (id=143)????
??????????????? count??? 1?????
??????? [10]??? HashMap$Entry? (id=136)?????
??????????? key??? "_0.fdx"?????
??????????? value??? IndexFileDeleter$RefCount? (id=144)????
??????????????? count??? 1?????
??????? [13]??? HashMap$Entry? (id=139)?????
??????????? key??? "_0.prx"?????
??????????? value??? IndexFileDeleter$RefCount? (id=145)????
??????????????? count??? 1?????
??????? [14]??? HashMap$Entry? (id=140)?????
??????????? key??? "_0.fdt"?????
??????????? value??? IndexFileDeleter$RefCount? (id=146)????
??????????????? count??? 1

然后會合并成compound文件,并加入引用計數


refCounts??? HashMap? (id=101)?????
??? size??? 10????
??? table??? HashMap$Entry[16]? (id=105)?????
??????? [1]??? HashMap$Entry? (id=129)?????
??????????? key??? "_0.tis"?????
??????????? value??? IndexFileDeleter$RefCount? (id=138)????
??????????????? count??? 1?????
??????? [2]??? HashMap$Entry? (id=154)?????
??????????? key??? "_0.cfs"?????
??????????? value??? IndexFileDeleter$RefCount? (id=155)????
??????????????? count??? 1
??????
??????? [3]??? HashMap$Entry? (id=130)?????
??????????? key??? "_0.fnm"?????
??????????? value??? IndexFileDeleter$RefCount? (id=141)????
??????????????? count??? 1?????
??????? [4]??? HashMap$Entry? (id=134)?????
??????????? key??? "_0.tii"?????
??????????? value??? IndexFileDeleter$RefCount? (id=142)????
??????????????? count??? 1?????
??????? [8]??? HashMap$Entry? (id=135)?????
??????????? key??? "_0.frq"?????
??????????? value??? IndexFileDeleter$RefCount? (id=143)????
??????????????? count??? 1?????
??????? [10]??? HashMap$Entry? (id=136)?????
??????????? key??? "_0.fdx"?????
??????????? value??? IndexFileDeleter$RefCount? (id=144)????
??????????????? count??? 1?????
??????? [13]??? HashMap$Entry? (id=139)?????
??????????? key??? "_0.prx"?????
??????????? value??? IndexFileDeleter$RefCount? (id=145)????
??????????????? count??? 1?????
??????? [14]??? HashMap$Entry? (id=140)?????
??????????? key??? "_0.fdt"?????
??????????? value??? IndexFileDeleter$RefCount? (id=146)????
??????????????? count??? 1

然后會用IndexFileDeleter.decRef()來刪除[_0.nrm, _0.tis, _0.fnm, _0.tii, _0.frq, _0.fdx, _0.prx, _0.fdt]文件


refCounts??? HashMap? (id=101)?????
??? size??? 2????
??? table??? HashMap$Entry[16]? (id=105)?????
??????? [2]??? HashMap$Entry? (id=154)?????
??????????? key??? "_0.cfs"?????
??????????? value??? IndexFileDeleter$RefCount? (id=155)????
??????????????? count??? 1?????
??????? [8]??? HashMap$Entry? (id=110)?????
??????????? key??? "segments_1"?????
??????????? value??? IndexFileDeleter$RefCount? (id=38)????
??????????????? count??? 1

然后為建立新的segments_2

?

?

refCounts??? HashMap? (id=77)?????
??? size??? 3????
??? table??? HashMap$Entry[16]? (id=84)?????
??????? [2]??? HashMap$Entry? (id=87)?????
??????????? key??? "_0.cfs"?????
??????????? value??? IndexFileDeleter$RefCount? (id=91)????
??????????????? count??? 3?????
??????? [8]??? HashMap$Entry? (id=89)?????
??????????? key??? "segments_1"?????
??????????? value??? IndexFileDeleter$RefCount? (id=62)????
??????????????? count??? 0?????
??????? [9]??? HashMap$Entry? (id=90)?????
??????????? key??? "segments_2"????
??????????? next??? null????
??????????? value??? IndexFileDeleter$RefCount? (id=93)????
??????????????? count??? 1

然后IndexFileDeleter.decRef() 刪除segments_1文件

?

refCounts??? HashMap? (id=77)?????
??? size??? 2????
??? table??? HashMap$Entry[16]? (id=84)?????
??????? [2]??? HashMap$Entry? (id=87)?????
??????????? key??? "_0.cfs"?????
??????????? value??? IndexFileDeleter$RefCount? (id=91)????
??????????????? count??? 2?????
??????? [9]??? HashMap$Entry? (id=90)?????
??????????? key??? "segments_2"?????
??????????? value??? IndexFileDeleter$RefCount? (id=93)????
??????????????? count??? 1

(3) 添加第二個段

indexDocs(writer, docDir);?
writer.commit();

??

(4) 添加第三個段,由于MergeFactor為3,則會進行一次段合并。

indexDocs(writer, docDir);?
writer.commit();

首先和其他的段一樣,生成_2.cfs以及segments_4

?

同時創建了一個線程來進行背后進行段合并(ConcurrentMergeScheduler$MergeThread.run())

?

這時候的引用計數如下

refCounts??? HashMap? (id=84)?????
??? size??? 5????
??? table??? HashMap$Entry[16]? (id=98)?????
??????? [2]??? HashMap$Entry? (id=112)?????
??????????? key??? "_0.cfs"?????
??????????? value??? IndexFileDeleter$RefCount? (id=117)????
??????????????? count??? 1?????
??????? [4]??? HashMap$Entry? (id=113)?????
??????????? key??? "_3.cfs"?????
??????????? value??? IndexFileDeleter$RefCount? (id=118)????
??????????????? count??? 1?????
??????? [12]??? HashMap$Entry? (id=114)?????
??????????? key??? "_1.cfs"?????
??????????? value??? IndexFileDeleter$RefCount? (id=119)????
??????????????? count??? 1?????
??????? [13]??? HashMap$Entry? (id=115)?????
??????????? key??? "_2.cfs"?????
??????????? value??? IndexFileDeleter$RefCount? (id=120)????
??????????????? count??? 1?????
??????? [15]??? HashMap$Entry? (id=116)?????
??????????? key??? "segments_4"?????
??????????? value??? IndexFileDeleter$RefCount? (id=121)????
??????????????? count??? 1???

(5) 關閉writer

writer.close();

通過IndexFileDeleter.decRef()刪除被合并的段

?

有關SimpleFSLock進行JVM之間的同步:

  • 有時候,我們寫java程序的時候,也需要不同的JVM之間進行同步,來保護一個整個系統中唯一的資源。
  • 如果唯一的資源僅僅在一個進程中,則可以使用線程同步的機制
  • 然而如果唯一的資源要被多個進程進行訪問,則需要進程間同步的機制,無論是Windows和Linux在操作系統層面都有很多的進程間同步的機制。
  • 但進程間的同步卻不是Java的特長,Lucene的SimpleFSLock給我們提供了一種方式。
Lock的抽象類?

public abstract class Lock {

? public static long LOCK_POLL_INTERVAL = 1000;

? public static final long LOCK_OBTAIN_WAIT_FOREVER = -1;

? public abstract boolean obtain() throws IOException;

? public boolean obtain(long lockWaitTimeout) throws LockObtainFailedException, IOException {

??? boolean locked = obtain();

??? if (lockWaitTimeout < 0 && lockWaitTimeout != LOCK_OBTAIN_WAIT_FOREVER)?
????? throw new IllegalArgumentException("...");

??? long maxSleepCount = lockWaitTimeout / LOCK_POLL_INTERVAL;

??? long sleepCount = 0;

??? while (!locked) {

????? if (lockWaitTimeout != LOCK_OBTAIN_WAIT_FOREVER && sleepCount++ >= maxSleepCount) {?
??????? throw new LockObtainFailedException("Lock obtain timed out.");?
????? }?
????? try {?
??????? Thread.sleep(LOCK_POLL_INTERVAL);?
????? } catch (InterruptedException ie) {?
??????? throw new ThreadInterruptedException(ie);?
????? }?
????? locked = obtain();?
??? }?
??? return locked;?
? }

? public abstract void release() throws IOException;

? public abstract boolean isLocked() throws IOException;

}

LockFactory的抽象類

public abstract class LockFactory {

? public abstract Lock makeLock(String lockName);

? abstract public void clearLock(String lockName) throws IOException;?
}

SimpleFSLock的實現類

class SimpleFSLock extends Lock {

? File lockFile;?
? File lockDir;

? public SimpleFSLock(File lockDir, String lockFileName) {?
??? this.lockDir = lockDir;?
??? lockFile = new File(lockDir, lockFileName);?
? }

? @Override?
? public boolean obtain() throws IOException {

??? if (!lockDir.exists()) {

????? if (!lockDir.mkdirs())?
??????? throw new IOException("Cannot create directory: " + lockDir.getAbsolutePath());

??? } else if (!lockDir.isDirectory()) {

????? throw new IOException("Found regular file where directory expected: " + lockDir.getAbsolutePath());?
??? }

??? return lockFile.createNewFile();

? }

? @Override?
? public void release() throws LockReleaseFailedException {

??? if (lockFile.exists() && !lockFile.delete())?
????? throw new LockReleaseFailedException("failed to delete " + lockFile);

? }

? @Override?
? public boolean isLocked() {

??? return lockFile.exists();

? }

}

SimpleFSLockFactory的實現類

public class SimpleFSLockFactory extends FSLockFactory {

? public SimpleFSLockFactory(String lockDirName) throws IOException {

??? setLockDir(new File(lockDirName));

? }

? @Override?
? public Lock makeLock(String lockName) {

??? if (lockPrefix != null) {

????? lockName = lockPrefix + "-" + lockName;

??? }

??? return new SimpleFSLock(lockDir, lockName);

? }

? @Override?
? public void clearLock(String lockName) throws IOException {

??? if (lockDir.exists()) {

????? if (lockPrefix != null) {

??????? lockName = lockPrefix + "-" + lockName;

????? }

????? File lockFile = new File(lockDir, lockName);

????? if (lockFile.exists() && !lockFile.delete()) {

??????? throw new IOException("Cannot delete " + lockFile);

????? }

??? }

? }

};

?

2、創建文檔Document對象,并加入域(Field)

代碼:

Document doc = new Document();

doc.add(new Field("path", f.getPath(), Field.Store.YES, Field.Index.NOT_ANALYZED));

doc.add(new Field("modified",DateTools.timeToString(f.lastModified(), DateTools.Resolution.MINUTE), Field.Store.YES, Field.Index.NOT_ANALYZED));

doc.add(new Field("contents", new FileReader(f)));

Document對象主要包括以下部分:

  • 此文檔的boost,默認為1,大于一說明比一般的文檔更加重要,小于一說明更不重要。
  • 一個ArrayList保存此文檔所有的域
  • 每一個域包括域名,域值,和一些標志位,和fnm,fdx,fdt中的描述相對應。

doc??? Document? (id=42)????
??? boost??? 1.0????
??? fields??? ArrayList? (id=44)????
??????? elementData??? Object[10]? (id=46)????
??????????? [0]??? Field? (id=48)????
??????????????? binaryLength??? 0????
??????????????? binaryOffset??? 0????
??????????????? boost??? 1.0????
??????????????? fieldsData??? "exampledocs//file01.txt"????
??????????????? isBinary??? false????
??????????????? isIndexed??? true????
??????????????? isStored??? true????
??????????????? isTokenized??? false????
??????????????? lazy??? false????
??????????????? name??? "path"????
??????????????? omitNorms??? false????
??????????????? omitTermFreqAndPositions??? false????
??????????????? storeOffsetWithTermVector??? false????
??????????????? storePositionWithTermVector??? false????
??????????????? storeTermVector??? false????
??????????????? tokenStream??? null????
??????????? [1]??? Field? (id=50)????
??????????????? binaryLength??? 0????
??????????????? binaryOffset??? 0????
??????????????? boost??? 1.0????
??????????????? fieldsData??? "200910240957"????
??????????????? isBinary??? false????
??????????????? isIndexed??? true????
??????????????? isStored??? true????
??????????????? isTokenized??? false????
??????????????? lazy??? false????
??????????????? name??? "modified"????
??????????????? omitNorms??? false????
??????????????? omitTermFreqAndPositions??? false????
??????????????? storeOffsetWithTermVector??? false????
??????????????? storePositionWithTermVector??? false????
??????????????? storeTermVector??? false????
??????????????? tokenStream??? null????
??????????? [2]??? Field? (id=52)????
??????????????? binaryLength??? 0????
??????????????? binaryOffset??? 0????
??????????????? boost??? 1.0????
??????????????? fieldsData??? FileReader? (id=58)????
??????????????? isBinary??? false????
??????????????? isIndexed??? true????
??????????????? isStored??? false????
??????????????? isTokenized??? true????
??????????????? lazy??? false????
??????????????? name??? "contents"????
??????????????? omitNorms??? false????
??????????????? omitTermFreqAndPositions??? false????
??????????????? storeOffsetWithTermVector??? false????
??????????????? storePositionWithTermVector??? false????
??????????????? storeTermVector??? false????
??????????????? tokenStream??? null?????
??????? modCount??? 3????
??????? size??? 3 ??


3、將文檔加入IndexWriter

代碼:

writer.addDocument(doc);?
-->IndexWriter.addDocument(Document doc, Analyzer analyzer)?
???? -->doFlush = docWriter.addDocument(doc, analyzer);?
????????? --> DocumentsWriter.updateDocument(Document, Analyzer, Term)?
注:--> 代表一級函數調用

IndexWriter繼而調用DocumentsWriter.addDocument,其又調用DocumentsWriter.updateDocument。

4、將文檔加入DocumentsWriter

代碼:

DocumentsWriter.updateDocument(Document doc, Analyzer analyzer, Term delTerm)?
-->(1) DocumentsWriterThreadState state = getThreadState(doc, delTerm);?
-->(2) DocWriter perDoc = state.consumer.processDocument();?
-->(3) finishDocument(state, perDoc);

DocumentsWriter對象主要包含以下幾部分:

  • 用于寫索引文件
    • IndexWriter writer;
    • Directory directory;
    • Similarity similarity:分詞器
    • String segment:當前的段名,每當flush的時候,將索引寫入以此為名稱的段。
IndexWriter.doFlushInternal()?
--> String segment = docWriter.getSegment();//return segment?
--> newSegment = new SegmentInfo(segment,……);?
--> docWriter.createCompoundFile(segment);//根據segment創建cfs文件。
  • ?
    • String docStoreSegment:存儲域所要寫入的目標段。(在索引文件格式一文中已經詳細描述)
    • int docStoreOffset:存儲域在目標段中的偏移量。
    • int nextDocID:下一篇添加到此索引的文檔ID號,對于同一個索引文件夾,此變量唯一,且同步訪問。
    • DocConsumer consumer; 這是整個索引過程的核心,是IndexChain整個索引鏈的源頭。

基本索引鏈:

對于一篇文檔的索引過程,不是由一個對象來完成的,而是用對象組合的方式形成的一個處理鏈,鏈上的每個對象僅僅處理索引過程的一部分,稱為索引鏈,由于后面還有其他的索引鏈,所以此處的索引鏈我稱為基本索引鏈。

DocConsumer consumer 類型為DocFieldProcessor,是整個索引鏈的源頭,包含如下部分:

  • 對索引域的處理
    • DocFieldConsumer consumer 類型為DocInverter,包含如下部分
      • InvertedDocConsumer consumer類型為TermsHash,包含如下部分
        • TermsHashConsumer consumer類型為FreqProxTermsWriter,負責寫freq, prox信息
        • TermsHash nextTermsHash
          • TermsHashConsumer consumer類型為TermVectorsTermsWriter,負責寫tvx, tvd, tvf信息
      • InvertedDocEndConsumer endConsumer 類型為NormsWriter,負責寫nrm信息
  • 對存儲域的處理
    • FieldInfos fieldInfos = new FieldInfos();
    • StoredFieldsWriter fieldsWriter負責寫fnm, fdt, fdx信息
  • 刪除文檔
    • BufferedDeletes deletesInRAM = new BufferedDeletes();
    • BufferedDeletes deletesFlushed = new BufferedDeletes();

類BufferedDeletes包含了一下的成員變量:

  • HashMap terms = new HashMap();刪除的詞(Term)
  • HashMap queries = new HashMap();刪除的查詢(Query)
  • List docIDs = new ArrayList();刪除的文檔ID
  • long bytesUsed:用于判斷是否應該對刪除的文檔寫入索引文件。

由此可見,文檔的刪除主要有三種方式:

  • IndexWriter.deleteDocuments(Term term):所有包含此詞的文檔都會被刪除。
  • IndexWriter.deleteDocuments(Query query):所有能滿足此查詢的文檔都會被刪除。
  • IndexReader.deleteDocument(int docNum):刪除此文檔ID

刪除文檔既可以用reader進行刪除,也可以用writer進行刪除,不同的是,reader進行刪除后,此reader馬上能夠生效,而用writer刪除后,會被緩存在deletesInRAM及deletesFlushed中,只有寫入到索引文件中,當reader再次打開的時候,才能夠看到。

那deletesInRAM和deletesFlushed各有什么用處呢?

此版本的Lucene對文檔的刪除是支持多線程的,當用IndexWriter刪除文檔的時候,都是緩存在deletesInRAM中的,直到flush,才將刪除的文檔寫入到索引文件中去,我們知道flush是需要一段時間的,那么在flush的過程中,另一個線程又有文檔刪除怎么辦呢?

一般過程是這個樣子的,當flush的時候,首先在同步(synchornized)的方法pushDeletes中,將deletesInRAM全部加到deletesFlushed中,然后將deletesInRAM清空,退出同步方法,于是flush的線程程就向索引文件寫deletesFlushed中的刪除文檔的過程,而與此同時其他線程新刪除的文檔則添加到新的deletesInRAM中去,直到下次flush才寫入索引文件。

  • 緩存管理
    • 為了提高索引的速度,Lucene對很多的數據進行了緩存,使一起寫入磁盤,然而緩存需要進行管理,何時分配,何時回收,何時寫入磁盤都需要考慮。
    • ArrayList freeCharBlocks = new ArrayList();將用于緩存詞(Term)信息的空閑塊
    • ArrayList freeByteBlocks = new ArrayList();將用于緩存文檔號(doc id)及詞頻(freq),位置(prox)信息的空閑塊。
    • ArrayList freeIntBlocks = new ArrayList();將存儲某詞的詞頻(freq)和位置(prox)分別在byteBlocks中的偏移量
    • boolean bufferIsFull;用來判斷緩存是否滿了,如果滿了,則應該寫入磁盤
    • long numBytesAlloc;分配的內存數量
    • long numBytesUsed;使用的內存數量
    • long freeTrigger;應該開始回收內存時的內存用量。
    • long freeLevel;回收內存應該回收到的內存用量。
    • long ramBufferSize;用戶設定的內存用量。
緩存用量之間的關系如下:?
DocumentsWriter.setRAMBufferSizeMB(double mb){?

??? ramBufferSize = (long) (mb*1024*1024);//用戶設定的內存用量,當使用內存大于此時,開始寫入磁盤?
??? waitQueuePauseBytes = (long) (ramBufferSize*0.1);?
??? waitQueueResumeBytes = (long) (ramBufferSize*0.05);?
??? freeTrigger = (long) (1.05 * ramBufferSize);//當分配的內存到達105%的時候開始釋放freeBlocks中的內存?
??? freeLevel = (long) (0.95 * ramBufferSize);//一直釋放到95%

}?

DocumentsWriter.balanceRAM(){?
??? if (numBytesAlloc+deletesRAMUsed > freeTrigger) {?
??? //當分配的內存加刪除文檔所占用的內存大于105%的時候,開始釋放內存?
??????? while(numBytesAlloc+deletesRAMUsed > freeLevel) {?
??????? //一直進行釋放,直到95%?

??????????? //釋放free blocks

??????????? byteBlockAllocator.freeByteBlocks.remove(byteBlockAllocator.freeByteBlocks.size()-1);?
??????????? numBytesAlloc -= BYTE_BLOCK_SIZE;

??????????? freeCharBlocks.remove(freeCharBlocks.size()-1);?
??????????? numBytesAlloc -= CHAR_BLOCK_SIZE * CHAR_NUM_BYTE;

??????????? freeIntBlocks.remove(freeIntBlocks.size()-1);?
??????????? numBytesAlloc -= INT_BLOCK_SIZE * INT_NUM_BYTE;?
??????? }?
??? } else {

??????? if (numBytesUsed+deletesRAMUsed > ramBufferSize){

??????? //當使用的內存加刪除文檔占有的內存大于用戶指定的內存時,可以寫入磁盤

????????????? bufferIsFull = true;

??????? }

??? }?
}

當判斷是否應該寫入磁盤時:

  • 如果使用的內存大于用戶指定內存時,bufferIsFull = true
  • 當使用的內存加刪除文檔所占的內存加正在寫入的刪除文檔所占的內存大于用戶指定內存時 deletesInRAM.bytesUsed + deletesFlushed.bytesUsed + numBytesUsed) >= ramBufferSize
  • 當刪除的文檔數目大于maxBufferedDeleteTerms時

DocumentsWriter.timeToFlushDeletes(){

??? return (bufferIsFull || deletesFull()) && setFlushPending();

}

DocumentsWriter.deletesFull(){

??? return (ramBufferSize != IndexWriter.DISABLE_AUTO_FLUSH &&?
??????? (deletesInRAM.bytesUsed + deletesFlushed.bytesUsed + numBytesUsed) >= ramBufferSize) ||?
??????? (maxBufferedDeleteTerms != IndexWriter.DISABLE_AUTO_FLUSH &&?
??????? ((deletesInRAM.size() + deletesFlushed.size()) >= maxBufferedDeleteTerms));

}

  • 多線程并發索引
    • 為了支持多線程并發索引,對每一個線程都有一個DocumentsWriterThreadState,其為每一個線程根據DocConsumer consumer的索引鏈來創建每個線程的索引鏈(XXXPerThread),來進行對文檔的并發處理。
    • DocumentsWriterThreadState[] threadStates = new DocumentsWriterThreadState[0];
    • HashMap threadBindings = new HashMap();
    • 雖然對文檔的處理過程可以并行,但是將文檔寫入索引文件卻必須串行進行,串行寫入的代碼在DocumentsWriter.finishDocument中
    • WaitQueue waitQueue = new WaitQueue()
    • long waitQueuePauseBytes
    • long waitQueueResumeBytes

在Lucene中,文檔是按添加的順序編號的,DocumentsWriter中的nextDocID就是記錄下一個添加的文檔id。 當Lucene支持多線程的時候,就必須要有一個synchornized方法來付給文檔id并且將nextDocID加一,這些是在DocumentsWriter.getThreadState這個函數里面做的。

雖然給文檔付ID沒有問題了。但是由Lucene索引文件格式我們知道,文檔是要按照ID的順序從小到大寫到索引文件中去的,然而不同的文檔處理速度不同,當一個先來的線程一處理一篇需要很長時間的大文檔時,另一個后來的線程二可能已經處理了很多小的文檔了,但是這些后來小文檔的ID號都大于第一個線程所處理的大文檔,因而不能馬上寫到索引文件中去,而是放到waitQueue中,僅僅當大文檔處理完了之后才寫入索引文件。

waitQueue中有一個變量nextWriteDocID表示下一個可以寫入文件的ID,當付給大文檔ID=4時,則nextWriteDocID也設為4,雖然后來的小文檔5,6,7,8等都已處理結束,但是如下代碼,

WaitQueue.add(){

??? if (doc.docID == nextWriteDocID){?
?????? …………?
??? } else {?
??????? waiting[loc] = doc;?
??????? waitingBytes += doc.sizeInBytes();?
?? }

?? doPause()

}

則把5, 6, 7, 8放入waiting隊列,并且記錄當前等待的文檔所占用的內存大小waitingBytes。

當大文檔4處理完畢后,不但寫入文檔4,把原來等待的文檔5, 6, 7, 8也一起寫入。

WaitQueue.add(){

??? if (doc.docID == nextWriteDocID) {

?????? writeDocument(doc);

?????? while(true) {

?????????? doc = waiting[nextWriteLoc];

?????????? writeDocument(doc);

?????? }

?? } else {

????? …………

?? }

?? doPause()

}

但是這存在一個問題:當大文檔很大很大,處理的很慢很慢的時候,后來的線程二可能已經處理了很多的小文檔了,這些文檔都是在waitQueue中,則占有了越來越多的內存,長此以往,有內存不夠的危險。

因而在finishDocuments里面,在WaitQueue.add最后調用了doPause()函數

DocumentsWriter.finishDocument(){

??? doPause = waitQueue.add(docWriter);

??? if (doPause)?
??????? waitForWaitQueue();

??? notifyAll();

}

WaitQueue.doPause() {?
??? return waitingBytes > waitQueuePauseBytes;?
}

當waitingBytes足夠大的時候(為用戶指定的內存使用量的10%),doPause返回true,于是后來的線程二會進入wait狀態,不再處理另外的文檔,而是等待線程一處理大文檔結束。

當線程一處理大文檔結束的時候,調用notifyAll喚醒等待他的線程。

DocumentsWriter.waitForWaitQueue() {?
? do {?
??? try {?
????? wait();?
??? } catch (InterruptedException ie) {?
????? throw new ThreadInterruptedException(ie);?
??? }?
? } while (!waitQueue.doResume());?
}

WaitQueue.doResume() {?
???? return waitingBytes <= waitQueueResumeBytes;?
}

當waitingBytes足夠小的時候,doResume返回true, 則線程二不用再wait了,可以繼續處理另外的文檔。

  • 一些標志位
    • int maxFieldLength:一篇文檔中,一個域內可索引的最大的詞(Term)數。
    • int maxBufferedDeleteTerms:可緩存的最大的刪除詞(Term)數。當大于這個數的時候,就要寫到文件中了。

此過程又包含如下三個子過程:

4.1、得到當前線程對應的文檔集處理對象(DocumentsWriterThreadState)

代碼為:

DocumentsWriterThreadState state = getThreadState(doc, delTerm);

在Lucene中,對于同一個索引文件夾,只能夠有一個IndexWriter打開它,在打開后,在文件夾中,生成文件write.lock,當其他IndexWriter再試圖打開此索引文件夾的時候,則會報org.apache.lucene.store.LockObtainFailedException錯誤。

這樣就出現了這樣一個問題,在同一個進程中,對同一個索引文件夾,只能有一個IndexWriter打開它,因而如果想多線程向此索引文件夾中添加文檔,則必須共享一個IndexWriter,而且在以往的實現中,addDocument函數是同步的(synchronized),也即多線程的索引并不能起到提高性能的效果。

于是為了支持多線程索引,不使IndexWriter成為瓶頸,對于每一個線程都有一個相應的文檔集處理對象(DocumentsWriterThreadState),這樣對文檔的索引過程可以多線程并行進行,從而增加索引的速度。

getThreadState函數是同步的(synchronized),DocumentsWriter有一個成員變量threadBindings,它是一個HashMap,鍵為線程對象(Thread.currentThread()),值為此線程對應的DocumentsWriterThreadState對象。

DocumentsWriterThreadState DocumentsWriter.getThreadState(Document doc, Term delTerm)包含如下幾個過程:

  • 根據當前線程對象,從HashMap中查找相應的DocumentsWriterThreadState對象,如果沒找到,則生成一個新對象,并添加到HashMap中
DocumentsWriterThreadState state = (DocumentsWriterThreadState) threadBindings.get(Thread.currentThread());?
if (state == null) {?
??? ……?
??? state = new DocumentsWriterThreadState(this);?
??? ……?
??? threadBindings.put(Thread.currentThread(), state);?
}?
  • 如果此線程對象正在用于處理上一篇文檔,則等待,直到此線程的上一篇文檔處理完。
DocumentsWriter.getThreadState() {?
??? waitReady(state);?
??? state.isIdle = false;?
}?

waitReady(state) {?
??? while (!state.isIdle) {wait();}?
}??

顯然如果state.isIdle為false,則此線程等待。?
在一篇文檔處理之前,state.isIdle = false會被設定,而在一篇文檔處理完畢之后,DocumentsWriter.finishDocument(DocumentsWriterThreadState perThread, DocWriter docWriter)中,會首先設定perThread.isIdle = true; 然后notifyAll()來喚醒等待此文檔完成的線程,從而處理下一篇文檔。
  • 如果IndexWriter剛剛commit過,則新添加的文檔要加入到新的段中(segment),則首先要生成新的段名。
initSegmentName(false);?
--> if (segment == null) segment = writer.newSegmentName();
  • 將此線程的文檔處理對象設為忙碌:state.isIdle = false;

4.2、用得到的文檔集處理對象(DocumentsWriterThreadState)處理文檔

代碼為:

DocWriter perDoc = state.consumer.processDocument();

每一個文檔集處理對象DocumentsWriterThreadState都有一個文檔及域處理對象DocFieldProcessorPerThread,它的成員函數processDocument()被調用來對文檔及域進行處理。

線程索引鏈(XXXPerThread):

由于要多線程進行索引,因而每個線程都要有自己的索引鏈,稱為線程索引鏈。

線程索引鏈同基本索引鏈有相似的樹形結構,由基本索引鏈中每個層次的對象調用addThreads進行創建的,負責每個線程的對文檔的處理。

DocFieldProcessorPerThread是線程索引鏈的源頭,由DocFieldProcessor.addThreads(…)創建

DocFieldProcessorPerThread對象結構如下:

  • 對索引域進行處理
    • DocFieldConsumerPerThread consumer 類型為 DocInverterPerThread,由DocInverter.addThreads創建
      • InvertedDocConsumerPerThread consumer 類型為TermsHashPerThread,由TermsHash.addThreads創建
        • TermsHashConsumerPerThread consumer類型為FreqProxTermsWriterPerThread,由FreqProxTermsWriter.addThreads創建,負責每個線程的freq,prox信息處理
        • TermsHashPerThread nextPerThread
          • TermsHashConsumerPerThread consumer類型TermVectorsTermsWriterPerThread,由TermVectorsTermsWriter創建,負責每個線程的tvx,tvd,tvf信息處理
      • InvertedDocEndConsumerPerThread endConsumer 類型為NormsWriterPerThread,由NormsWriter.addThreads創建,負責nrm信息的處理
  • 對存儲域進行處理
    • StoredFieldsWriterPerThread fieldsWriter由StoredFieldsWriter.addThreads創建,負責fnm,fdx,fdt的處理。
    • FieldInfos fieldInfos;

DocumentsWriter.DocWriter DocFieldProcessorPerThread.processDocument()包含以下幾個過程:

4.2.1、開始處理當前文檔

consumer(DocInverterPerThread).startDocument();?
fieldsWriter(StoredFieldsWriterPerThread).startDocument();

在此版的Lucene中,幾乎所有的XXXPerThread的類,都有startDocument和finishDocument兩個函數,因為對同一個線程,這些對象都是復用的,而非對每一篇新來的文檔都創建一套,這樣也提高了效率,也牽扯到數據的清理問題。一般在startDocument函數中,清理處理上篇文檔遺留的數據,在finishDocument中,收集本次處理的結果數據,并返回,一直返回到DocumentsWriter.updateDocument(Document, Analyzer, Term) 然后根據條件判斷是否將數據刷新到硬盤上。

4.2.2、逐個處理文檔的每一個域

由于一個線程可以連續處理多個文檔,而在普通的應用中,幾乎每篇文檔的域都是大致相同的,為每篇文檔的每個域都創建一個處理對象非常低效,因而考慮到復用域處理對象DocFieldProcessorPerField,對于每一個域都有一個此對象。

那當來到一個新的域的時候,如何更快的找到此域的處理對象呢?Lucene創建了一個DocFieldProcessorPerField[] fieldHash哈希表來方便更快查找域對應的處理對象。

當處理各個域的時候,按什么順序呢?其實是按照域名的字典順序。因而Lucene創建了DocFieldProcessorPerField[] fields的數組來方便按順序處理域。

因而一個域的處理對象被放在了兩個地方。

對于域的處理過程如下:

4.2.2.1、首先:對于每一個域,按照域名,在fieldHash中查找域處理對象DocFieldProcessorPerField,代碼如下:

final int hashPos = fieldName.hashCode() & hashMask;//計算哈希值?
DocFieldProcessorPerField fp = fieldHash[hashPos];//找到哈希表中對應的位置?
while(fp != null && !fp.fieldInfo.name.equals(fieldName)) fp = fp.next;//鏈式哈希表

如果能夠找到,則更新DocFieldProcessorPerField中的域信息fp.fieldInfo.update(field.isIndexed()…)

如果沒有找到,則添加域到DocFieldProcessorPerThread.fieldInfos中,并創建新的DocFieldProcessorPerField,且將其加入哈希表。代碼如下:

fp = new DocFieldProcessorPerField(this, fi);?
fp.next = fieldHash[hashPos];?
fieldHash[hashPos] = fp;

如果是一個新的field,則將其加入fields數組fields[fieldCount++] = fp;

并且如果是存儲域的話,用StoredFieldsWriterPerThread將其寫到索引中:

if (field.isStored()) {?
? fieldsWriter.addField(field, fp.fieldInfo);?
}

4.2.2.1.1、處理存儲域的過程如下:

StoredFieldsWriterPerThread.addField(Fieldable field, FieldInfo fieldInfo)?
--> localFieldsWriter.writeField(fieldInfo, field);

FieldsWriter.writeField(FieldInfo fi, Fieldable field)代碼如下:

請參照fdt文件的格式,則一目了然:

fieldsStream.writeVInt(fi.number);//文檔號?
byte bits = 0;?
if (field.isTokenized())?
??? bits |= FieldsWriter.FIELD_IS_TOKENIZED;?
if (field.isBinary())?
??? bits |= FieldsWriter.FIELD_IS_BINARY;?
if (field.isCompressed())?
??? bits |= FieldsWriter.FIELD_IS_COMPRESSED;

fieldsStream.writeByte(bits); //域的屬性位

if (field.isCompressed()) {//對于壓縮域?
??? // compression is enabled for the current field?
??? final byte[] data;?
??? final int len;?
??? final int offset;?
??? // check if it is a binary field?
??? if (field.isBinary()) {?
??????? data = CompressionTools.compress(field.getBinaryValue(), field.getBinaryOffset(), field.getBinaryLength());?
??? } else {?
??????? byte x[] = field.stringValue().getBytes("UTF-8");?
??????? data = CompressionTools.compress(x, 0, x.length);?
??? }?
??? len = data.length;?
??? offset = 0;?
??? fieldsStream.writeVInt(len);//寫長度?
??? fieldsStream.writeBytes(data, offset, len);//寫二進制內容?
} else {//對于非壓縮域?
??? // compression is disabled for the current field?
??? if (field.isBinary()) {//如果是二進制域?
??????? final byte[] data;?
??????? final int len;?
??????? final int offset;?
??????? data = field.getBinaryValue();?
??????? len = field.getBinaryLength();?
??????? offset = field.getBinaryOffset();

??????? fieldsStream.writeVInt(len);//寫長度?
??????? fieldsStream.writeBytes(data, offset, len);//寫二進制內容?
??? } else {?
??????? fieldsStream.writeString(field.stringValue());//寫字符內容?
??? }?
}

4.2.2.2、然后:對fields數組進行排序,是域按照名稱排序。quickSort(fields, 0, fieldCount-1);

4.2.2.3、最后:按照排序號的順序,對域逐個處理,此處處理的僅僅是索引域,代碼如下:

for(int i=0;i????? fields[i].consumer.processFields(fields[i].fields, fields[i].fieldCount);

域處理對象(DocFieldProcessorPerField)結構如下:

域索引鏈:

每個域也有自己的索引鏈,稱為域索引鏈,每個域的索引鏈也有同線程索引鏈有相似的樹形結構,由線程索引鏈中每個層次的每個層次的對象調用addField進行創建,負責對此域的處理。

和基本索引鏈及線程索引鏈不同的是,域索引鏈僅僅負責處理索引域,而不負責存儲域的處理。

DocFieldProcessorPerField是域索引鏈的源頭,對象結構如下:

  • DocFieldConsumerPerField consumer類型為DocInverterPerField,由DocInverterPerThread.addField創建
    • InvertedDocConsumerPerField consumer 類型為TermsHashPerField,由TermsHashPerThread.addField創建
      • TermsHashConsumerPerField consumer 類型為FreqProxTermsWriterPerField,由FreqProxTermsWriterPerThread.addField創建,負責freq, prox信息的處理
      • TermsHashPerField nextPerField
        • TermsHashConsumerPerField consumer 類型為TermVectorsTermsWriterPerField,由TermVectorsTermsWriterPerThread.addField創建,負責tvx, tvd, tvf信息的處理
    • InvertedDocEndConsumerPerField endConsumer 類型為NormsWriterPerField,由NormsWriterPerThread.addField創建,負責nrm信息的處理。

4.2.2.3.1、處理索引域的過程如下:

DocInverterPerField.processFields(Fieldable[], int) 過程如下:

  • 判斷是否要形成倒排表,代碼如下:
boolean doInvert = consumer.start(fields, count);?
--> TermsHashPerField.start(Fieldable[], int)??
????? --> for(int i=0;i???????????? if (fields[i].isIndexed())?
???????????????? return true;?
??????????? return false;

讀到這里,大家可能會發生困惑,既然XXXPerField是對于每一個域有一個處理對象的,那為什么參數傳進來的是Fieldable[]數組, 并且還有域的數目count呢?

其實這不經常用到,但必須得提一下,由上面的fieldHash的實現我們可以看到,是根據域名進行哈希的,所以準確的講,XXXPerField并非對于每一個域有一個處理對象,而是對每一組相同名字的域有相同的處理對象。

對于同一篇文檔,相同名稱的域可以添加多個,代碼如下:

doc.add(new Field("contents", "the content of the file.", Field.Store.NO, Field.Index.NOT_ANALYZED));?
doc.add(new Field("contents", new FileReader(f)));

則傳進來的名為"contents"的域如下:

fields??? Fieldable[2]? (id=52)????
??? [0]??? Field? (id=56)????
??????? binaryLength??? 0????
??????? binaryOffset??? 0????
??????? boost??? 1.0????
??????? fieldsData??? "the content of the file."????
??????? isBinary??? false????
??????? isCompressed??? false????
??????? isIndexed??? true????
??????? isStored??? false????
??????? isTokenized??? false????
??????? lazy??? false????
??????? name??? "contents"????
??????? omitNorms??? false????
??????? omitTermFreqAndPositions??? false????
??????? storeOffsetWithTermVector??? false????
??????? storePositionWithTermVector??? false????
??????? storeTermVector??? false????
??????? tokenStream??? null????
??? [1]??? Field? (id=58)????
??????? binaryLength??? 0????
??????? binaryOffset??? 0????
??????? boost??? 1.0????
??????? fieldsData??? FileReader? (id=131)????
??????? isBinary??? false????
??????? isCompressed??? false????
??????? isIndexed??? true????
??????? isStored??? false????
??????? isTokenized??? true????
??????? lazy??? false????
??????? name??? "contents"????
??????? omitNorms??? false????
??????? omitTermFreqAndPositions??? false????
??????? storeOffsetWithTermVector??? false????
??????? storePositionWithTermVector??? false????
??????? storeTermVector??? false????
??????? tokenStream??? null???

  • 對傳進來的同名域逐一處理,代碼如下

for(int i=0;i

??? final Fieldable field = fields[i];

??? if (field.isIndexed() && doInvert) {

??????? //僅僅對索引域進行處理

??????? if (!field.isTokenized()) {

??????????? //如果此域不分詞,見(1)對不分詞的域的處理

??????? } else {

??????????? //如果此域分詞,見(2)對分詞的域的處理

??????? }

??? }

}

(1) 對不分詞的域的處理

(1-1) 得到域的內容,并構建單個Token形成的SingleTokenAttributeSource。因為不進行分詞,因而整個域的內容算做一個Token.

String stringValue = field.stringValue(); //stringValue??? "200910240957"??
final int valueLength = stringValue.length();?
perThread.singleToken.reinit(stringValue, 0, valueLength);

對于此域唯一的一個Token有以下的屬性:

  • Term:文字信息。在處理過程中,此值將保存在TermAttribute的實現類實例化的對象TermAttributeImp里面。
  • Offset:偏移量信息,是按字或字母的起始偏移量和終止偏移量,表明此Token在文章中的位置,多用于加亮。在處理過程中,此值將保存在OffsetAttribute的實現類實例化的對象OffsetAttributeImp里面。

在SingleTokenAttributeSource里面,有一個HashMap來保存可能用于保存屬性的類名(Key,準確的講是接口)以及保存屬性信息的對象(Value):

singleToken??? DocInverterPerThread$SingleTokenAttributeSource? (id=150)????
??? attributeImpls??? LinkedHashMap? (id=945)????
??? attributes??? LinkedHashMap? (id=946)?????
??????? size??? 2????
??????? table??? HashMap$Entry[16]? (id=988)????
??????????? [0]??? LinkedHashMap$Entry? (id=991)?????
??????????????? key??? Class (org.apache.lucene.analysis.tokenattributes.TermAttribute) (id=755)?????
??????????????? value????TermAttributeImpl? (id=949)????
????????????????????termBuffer??? char[19]? (id=954)??? //[2, 0, 0, 9, 1, 0, 2, 4, 0, 9, 5, 7]?
??????????????????? termLength??? 12
?????
??????????? [7]??? LinkedHashMap$Entry? (id=993)?????
??????????????? key??? Class (org.apache.lucene.analysis.tokenattributes.OffsetAttribute) (id=274)?????
??????????????? value????OffsetAttributeImpl? (id=948)????
????????????????????endOffset??? 12????
??????????????????? startOffset??? 0
?????
??? factory??? AttributeSource$AttributeFactory$DefaultAttributeFactory? (id=947)????
??? offsetAttribute??? OffsetAttributeImpl? (id=948)????
??? termAttribute??? TermAttributeImpl? (id=949)???

(1-2) 得到Token的各種屬性信息,為索引做準備。

consumer.start(field)做的主要事情就是根據各種屬性的類型來構造保存屬性的對象(HashMap中有則取出,無則構造),為索引做準備。

consumer(TermsHashPerField).start(…)

--> termAtt = fieldState.attributeSource.addAttribute(TermAttribute.class);得到的就是上述HashMap中的TermAttributeImpl???

--> consumer(FreqProxTermsWriterPerField).start(f);

????? --> if (fieldState.attributeSource.hasAttribute(PayloadAttribute.class)) {

??????????????? payloadAttribute = fieldState.attributeSource.getAttribute(PayloadAttribute.class);?
??????????????? 存儲payload信息則得到payload的屬}

--> nextPerField(TermsHashPerField).start(f);

????? --> termAtt = fieldState.attributeSource.addAttribute(TermAttribute.class);得到的還是上述HashMap中的TermAttributeImpl

????? --> consumer(TermVectorsTermsWriterPerField).start(f);

??????????? --> if (doVectorOffsets) {

????????????????????? offsetAttribute = fieldState.attributeSource.addAttribute(OffsetAttribute.class);?
????????????????????? 如果存儲詞向量則得到的是上述HashMap中的OffsetAttributeImp }

(1-3) 將Token加入倒排表

consumer(TermsHashPerField).add();

加入倒排表的過程,無論對于分詞的域和不分詞的域,過程是一樣的,因而放到對分詞的域的解析中一起說明。

(2) 對分詞的域的處理

(2-1) 構建域的TokenStream

final TokenStream streamValue = field.tokenStreamValue();

//用戶可以在添加域的時候,應用構造函數public Field(String name, TokenStream tokenStream) 直接傳進一個TokenStream過來,這樣就不用另外構建一個TokenStream了。

if (streamValue != null)?
? stream = streamValue;?
else {

? ……

? stream = docState.analyzer.reusableTokenStream(fieldInfo.name, reader);

}

此時TokenStream的各項屬性值還都是空的,等待一個一個被分詞后得到,此時的TokenStream對象如下:

stream??? StopFilter? (id=112)????
??? attributeImpls??? LinkedHashMap? (id=121)????
??? attributes??? LinkedHashMap? (id=122)?????
??????? size??? 4????
??????? table??? HashMap$Entry[16]? (id=146)?????
??????????? [2]??? LinkedHashMap$Entry? (id=148)?????
??????????????? key??? Class (org.apache.lucene.analysis.tokenattributes.TypeAttribute) (id=154)?????
??????????????? value????TypeAttributeImpl? (id=157)????
????????????????????type??? "word"?????
??????????? [8]??? LinkedHashMap$Entry? (id=150)????
??????????????? after??? LinkedHashMap$Entry? (id=156)?????
??????????????????? key??? Class (org.apache.lucene.analysis.tokenattributes.OffsetAttribute) (id=163)?????
??????????????????? value????OffsetAttributeImpl? (id=164)????
????????????????????????endOffset??? 0????
??????????????????????? startOffset??? 0
?????
??????????????? key??? Class (org.apache.lucene.analysis.tokenattributes.TermAttribute) (id=142)?????
??????????????? value????TermAttributeImpl? (id=133)????
????????????????????termBuffer??? char[17]? (id=173)????
??????????????????? termLength??? 0
?????
??????????? [10]??? LinkedHashMap$Entry? (id=151)?????
??????????????? key??? Class (org.apache.lucene.analysis.tokenattributes.PositionIncrementAttribute) (id=136)?????
??????????????? value????PositionIncrementAttributeImpl? (id=129)????
????????????????????positionIncrement??? 1?????
??? currentState??? AttributeSource$State? (id=123)????
??? enablePositionIncrements??? true????
??? factory??? AttributeSource$AttributeFactory$DefaultAttributeFactory? (id=125)????
??? input????LowerCaseFilter? (id=127)?????
??????? input????StandardFilter? (id=213)?????
??????????? input????StandardTokenizer? (id=218)?????
??????????????? input????FileReader? (id=93)??? //從文件中讀出來的文本,將經過分詞器分詞,并一層層的Filter的處理,得到一個個Token?
??? stopWords??? CharArraySet$UnmodifiableCharArraySet? (id=131)????
??? termAtt??? TermAttributeImpl? (id=133)???

(2-2) 得到第一個Token,并初始化此Token的各項屬性信息,并為索引做準備(start)。

boolean hasMoreTokens = stream.incrementToken();//得到第一個Token

OffsetAttribute offsetAttribute = fieldState.attributeSource.addAttribute(OffsetAttribute.class);//得到偏移量屬性

offsetAttribute??? OffsetAttributeImpl? (id=164)????
??? endOffset??? 8????
??? startOffset??? 0???

PositionIncrementAttribute posIncrAttribute = fieldState.attributeSource.addAttribute(PositionIncrementAttribute.class);//得到位置屬性

posIncrAttribute??? PositionIncrementAttributeImpl? (id=129)????
??? positionIncrement??? 1???

consumer.start(field);//其中得到了TermAttribute屬性,如果存儲payload則得到PayloadAttribute屬性,如果存儲詞向量則得到OffsetAttribute屬性。

(2-3) 進行循環,不斷的取下一個Token,并添加到倒排表

for(;;) {

??? if (!hasMoreTokens) break;

??? ……?
??? consumer.add();

??? ……?
??? hasMoreTokens = stream.incrementToken();?
}

(2-4) 添加Token到倒排表的過程consumer(TermsHashPerField).add()

TermsHashPerField對象主要包括以下部分:

  • CharBlockPool charPool; 用于存儲Token的文本信息,如果不足時,從DocumentsWriter中的freeCharBlocks分配
  • ByteBlockPool bytePool;用于存儲freq, prox信息,如果不足時,從DocumentsWriter中的freeByteBlocks分配
  • IntBlockPool intPool; 用于存儲分別指向每個Token在bytePool中freq和prox信息的偏移量。如果不足時,從DocumentsWriter的freeIntBlocks分配
  • TermsHashConsumerPerField consumer類型為FreqProxTermsWriterPerField,用于寫freq, prox信息到緩存中。
  • RawPostingList[] postingsHash = new RawPostingList[postingsHashSize];存儲倒排表,每一個Term都有一個RawPostingList (PostingList),其中包含了int textStart,也即文本在charPool中的偏移量,int byteStart,即此Term的freq和prox信息在bytePool中的起始偏移量,int intStart,即此term的在intPool中的起始偏移量。

形成倒排表的過程如下:

//得到token的文本及文本長度

final char[] tokenText = termAtt.termBuffer();//[s, t, u, d, e, n, t, s]

final int tokenTextLen = termAtt.termLength();//tokenTextLen 8

//按照token的文本計算哈希值,以便在postingsHash中找到此token對應的倒排表

int downto = tokenTextLen;?
int code = 0;?
while (downto > 0) {?
? char ch = tokenText[—downto];?
? code = (code*31) + ch;?
}

int hashPos = code & postingsHashMask;

//在倒排表哈希表中查找此Token,如果找到相應的位置,但是不是此Token,說明此位置存在哈希沖突,采取重新哈希rehash的方法。

p = postingsHash[hashPos];

if (p != null && !postingEquals(tokenText, tokenTextLen)) {??
? final int inc = ((code>>8)+code)|1;?
? do {?
??? code += inc;?
??? hashPos = code & postingsHashMask;?
??? p = postingsHash[hashPos];?
? } while (p != null && !postingEquals(tokenText, tokenTextLen));?
}

//如果此Token之前從未出現過

if (p == null) {

??? if (textLen1 + charPool.charUpto > DocumentsWriter.CHAR_BLOCK_SIZE) {

??????? //當charPool不足的時候,在freeCharBlocks中分配新的buffer

??????? charPool.nextBuffer();

??? }

??? //從空閑的倒排表中分配新的倒排表

??? p = perThread.freePostings[--perThread.freePostingsCount];

??? //將文本復制到charPool中

??? final char[] text = charPool.buffer;?
??? final int textUpto = charPool.charUpto;?
??? p.textStart = textUpto + charPool.charOffset;?
??? charPool.charUpto += textLen1;?
??? System.arraycopy(tokenText, 0, text, textUpto, tokenTextLen);?
??? text[textUpto+tokenTextLen] = 0xffff;

??? //將倒排表放入哈希表中

??? postingsHash[hashPos] = p;?
??? numPostings++;

??? if (numPostingInt + intPool.intUpto > DocumentsWriter.INT_BLOCK_SIZE) intPool.nextBuffer();

??? //當intPool不足的時候,在freeIntBlocks中分配新的buffer。

??? if (DocumentsWriter.BYTE_BLOCK_SIZE - bytePool.byteUpto < numPostingInt*ByteBlockPool.FIRST_LEVEL_SIZE)

??????? bytePool.nextBuffer();

??? //當bytePool不足的時候,在freeByteBlocks中分配新的buffer。

??? //此處streamCount為2,表明在intPool中,每兩項表示一個詞,一個是指向bytePool中freq信息偏移量的,一個是指向bytePool中prox信息偏移量的。

??? intUptos = intPool.buffer;?
??? intUptoStart = intPool.intUpto;?
??? intPool.intUpto += streamCount;

??? p.intStart = intUptoStart + intPool.intOffset;

??? //在bytePool中分配兩個空間,一個放freq信息,一個放prox信息的。??
??? for(int i=0;i

??????? final int upto = bytePool.newSlice(ByteBlockPool.FIRST_LEVEL_SIZE);?
??????? intUptos[intUptoStart+i] = upto + bytePool.byteOffset;?
??? }?
??? p.byteStart = intUptos[intUptoStart];

??? //當Term原來沒有出現過的時候,調用newTerm

??? consumer(FreqProxTermsWriterPerField).newTerm(p);

}

//如果此Token之前曾經出現過,則調用addTerm。

else {

??? intUptos = intPool.buffers[p.intStart >> DocumentsWriter.INT_BLOCK_SHIFT];?
??? intUptoStart = p.intStart & DocumentsWriter.INT_BLOCK_MASK;?
??? consumer(FreqProxTermsWriterPerField).addTerm(p);

}

(2-5) 添加新Term的過程,consumer(FreqProxTermsWriterPerField).newTerm

final void newTerm(RawPostingList p0) {?
? FreqProxTermsWriter.PostingList p = (FreqProxTermsWriter.PostingList) p0;?
? p.lastDocID = docState.docID; //當一個新的term出現的時候,包含此Term的就只有本篇文檔,記錄其ID?
? p.lastDocCode = docState.docID << 1; //docCode是文檔ID左移一位,為什么左移,請參照索引文件格式(1)中的或然跟隨規則。?
? p.docFreq = 1; //docFreq這里用詞可能容易引起誤會,docFreq這里指的是此文檔所包含的此Term的次數,并非包含此Term的文檔的個數。?
? writeProx(p, fieldState.position); //寫入prox信息到bytePool中,此時freq信息還不能寫入,因為當前的文檔還沒有處理完,尚不知道此文檔包含此Term的總數。?
}

writeProx(FreqProxTermsWriter.PostingList p, int proxCode) {

? termsHashPerField.writeVInt(1, proxCode<<1);//第一個參數所謂1,也就是寫入此文檔在intPool中的第1項——prox信息。為什么左移一位呢?是因為后面可能跟著payload信息,參照索引文件格式(1)中或然跟隨規則。?
? p.lastPosition = fieldState.position;//總是要記錄lastDocID, lastPostion,是因為要計算差值,參照索引文件格式(1)中的差值規則。

}

(2-6) 添加已有Term的過程

final void addTerm(RawPostingList p0) {

? FreqProxTermsWriter.PostingList p = (FreqProxTermsWriter.PostingList) p0;

? if (docState.docID != p.lastDocID) {

????? //當文檔ID變了的時候,說明上一篇文檔已經處理完畢,可以寫入freq信息了。

????? //第一個參數所謂0,也就是寫入上一篇文檔在intPool中的第0項——freq信息。至于信息為何這樣寫,參照索引文件格式(1)中的或然跟隨規則,及tis文件格式。

????? if (1 == p.docFreq)?
??????? termsHashPerField.writeVInt(0, p.lastDocCode|1);?
????? else {?
??????? termsHashPerField.writeVInt(0, p.lastDocCode);?
??????? termsHashPerField.writeVInt(0, p.docFreq);?
????? }?
????? p.docFreq = 1;//對于新的文檔,freq還是為1.?
????? p.lastDocCode = (docState.docID - p.lastDocID) << 1;//文檔號存儲差值?
????? p.lastDocID = docState.docID;?
????? writeProx(p, fieldState.position);??
??? } else {

????? //當文檔ID不變的時候,說明此文檔中這個詞又出現了一次,從而freq加一,寫入再次出現的位置信息,用差值。?
????? p.docFreq++;?
????? writeProx(p, fieldState.position-p.lastPosition);?
? }?
}

(2-7) 結束處理當前域

consumer(TermsHashPerField).finish();

--> FreqProxTermsWriterPerField.finish()

--> TermVectorsTermsWriterPerField.finish()

endConsumer(NormsWriterPerField).finish();

--> norms[upto] = Similarity.encodeNorm(norm);//計算標準化因子的值。

--> docIDs[upto] = docState.docID;

4.2.3、結束處理當前文檔

final DocumentsWriter.DocWriter one = fieldsWriter(StoredFieldsWriterPerThread).finishDocument();

存儲域返回結果:一個寫成了二進制的存儲域緩存。

one??? StoredFieldsWriter$PerDoc? (id=322)????
??? docID??? 0????
??? fdt??? RAMOutputStream? (id=325)????
??????? bufferLength??? 1024????
??????? bufferPosition??? 40????
??????? bufferStart??? 0????
??????? copyBuffer??? null????
??????? currentBuffer??? byte[1024]? (id=332)????
??????? currentBufferIndex??? 0????
??????? file??? RAMFile? (id=333)????
??????? utf8Result??? UnicodeUtil$UTF8Result? (id=335)????
??? next??? null????
??? numStoredFields??? 2????
??? this$0??? StoredFieldsWriter? (id=327)???

final DocumentsWriter.DocWriter two = consumer(DocInverterPerThread).finishDocument();

--> NormsWriterPerThread.finishDocument()

--> TermsHashPerThread.finishDocument()

索引域的返回結果為null

4.3、用DocumentsWriter.finishDocument結束本次文檔添加

代碼:

DocumentsWriter.updateDocument(Document, Analyzer, Term)

--> DocumentsWriter.finishDocument(DocumentsWriterThreadState, DocumentsWriter$DocWriter)

????? --> doPause = waitQueue.add(docWriter);//有關waitQueue,在DocumentsWriter的緩存管理中已作解釋

??????????? --> DocumentsWriter$WaitQueue.writeDocument(DocumentsWriter$DocWriter)

????????????????? --> StoredFieldsWriter$PerDoc.finish()

??????????????????????? --> fieldsWriter.flushDocument(perDoc.numStoredFields, perDoc.fdt);將存儲域信息真正寫入文件。


5、DocumentsWriter對CharBlockPool,ByteBlockPool,IntBlockPool的緩存管理

  • 在索引的過程中,DocumentsWriter將詞信息(term)存儲在CharBlockPool中,將文檔號(doc ID),詞頻(freq)和位置(prox)信息存儲在ByteBlockPool中。
  • 在ByteBlockPool中,緩存是分塊(slice)分配的,塊(slice)是分層次的,層次越高,此層的塊越大,每一層的塊大小事相同的。
    • nextLevelArray表示的是當前層的下一層是第幾層,可見第9層的下一層還是第9層,也就是說最高有9層。
    • levelSizeArray表示每一層的塊大小,第一層是5個byte,第二層是14個byte以此類推。

ByteBlockPool類中有以下靜態變量:

final static int[] nextLevelArray = {1, 2, 3, 4, 5, 6, 7, 8, 9, 9};?
final static int[] levelSizeArray = {5, 14, 20, 30, 40, 40, 80, 80, 120, 200};

  • 在ByteBlockPool中分配一個塊的代碼如下:

?

//此函數僅僅在upto已經是當前塊的結尾的時候方才調用來分配新塊。

public int allocSlice(final byte[] slice, final int upto) {

? //可根據塊的結束符來得到塊所在的層次。從而我們可以推斷,每個層次的塊都有不同的結束符,第1層為16,第2層位17,第3層18,依次類推。

? final int level = slice[upto] & 15;

? //從數組總得到下一個層次及下一層塊的大小。

? final int newLevel = nextLevelArray[level];

? final int newSize = levelSizeArray[newLevel];

? // 如果當前緩存總量不夠大,則從DocumentsWriter的freeByteBlocks中分配。

? if (byteUpto > DocumentsWriter.BYTE_BLOCK_SIZE-newSize)

??? nextBuffer();

? final int newUpto = byteUpto;

? final int offset = newUpto + byteOffset;

? byteUpto += newSize;

? //當分配了新的塊的時候,需要有一個指針從本塊指向下一個塊,使得讀取此信息的時候,能夠在此塊讀取結束后,到下一個塊繼續讀取。

? //這個指針需要4個byte,在本塊中,除了結束符所占用的一個byte之外,之前的三個byte的數據都應該移到新的塊中,從而四個byte連起來形成一個指針。

? buffer[newUpto] = slice[upto-3];

? buffer[newUpto+1] = slice[upto-2];

? buffer[newUpto+2] = slice[upto-1];

? // 將偏移量(也即指針)寫入到連同結束符在內的四個byte

? slice[upto-3] = (byte) (offset >>> 24);

? slice[upto-2] = (byte) (offset >>> 16);

? slice[upto-1] = (byte) (offset >>> 8);

? slice[upto] = (byte) offset;

? // 在新的塊的結尾寫入新的結束符,結束符和層次的關系就是(endbyte = 16 | level)

? buffer[byteUpto-1] = (byte) (16|newLevel);

? return newUpto+3;

}

  • 在ByteBlockPool中,文檔號和詞頻(freq)信息是應用或然跟隨原則寫到一個塊中去的,而位置信息(prox)是寫入到另一個塊中去的,對于同一個詞,這兩塊的偏移量保存在IntBlockPool中。因而在IntBlockPool中,每一個詞都有兩個int,第0個表示docid + freq在ByteBlockPool中的偏移量,第1個表示prox在ByteBlockPool中的偏移量。
  • 在寫入docid + freq信息的時候,調用termsHashPerField.writeVInt(0, p.lastDocCode),第一個參數表示向此詞的第0個偏移量寫入;在寫入prox信息的時候,調用termsHashPerField.writeVInt(1, (proxCode<<1)|1),第一個參數表示向此詞的第1個偏移量寫入。
  • CharBlockPool是按照出現的先后順序保存詞(term)
  • 在TermsHashPerField中,有一個成員變量RawPostingList[] postingsHash,為每一個term分配了一個RawPostingList,將上述三個緩存關聯起來。

?

abstract class RawPostingList {

? final static int BYTES_SIZE = DocumentsWriter.OBJECT_HEADER_BYTES + 3*DocumentsWriter.INT_NUM_BYTE;

? int textStart; //此詞在CharBlockPool中的偏移量,由此可以知道是哪個詞。

? int intStart; //此詞在IntBlockPool中的偏移量,在指向的位置有兩個int,一個是docid + freq信息的偏移量,一個是prox信息的偏移量。

? int byteStart; //此詞在ByteBlockPool中的起始偏移量

}

static final class PostingList extends RawPostingList {

? int docFreq;??????????????????????????????????? // 此詞在此文檔中出現的次數

? int lastDocID;????????????????????????????????? // 上次處理完的包含此詞的文檔號。

? int lastDocCode;??????????????????????????????? // 文檔號和詞頻按照或然跟隨原則形成的編碼

? int lastPosition;?????????????????????????????? // 上次處理完的此詞的位置

}

這里需要說明的是,在IntBlockPool中保存了兩個在ByteBlockPool中的偏移量,而在RawPostingList的byteStart又保存了在ByteBlockPool中的偏移量,這兩者有什么區別呢?

在IntBlockPool中保存的分別指向docid+freq及prox信息在ByteBlockPool中的偏移量是主要用來寫入信息的,它記錄的偏移量是下一個要寫入的docid+freq或者prox在ByteBlockPool中的位置,隨著信息的不斷寫入,IntBlockPool中的兩個偏移量是不斷改變的,始終指向下一個可以寫入的位置。

RawPostingList中byteStart主要是用來讀取docid及prox信息的,當索引過程基本結束,所有的信息都寫入在緩存中了,那么如何找到此詞對應的文檔號偏移量及位置信息,然后寫到索引文件中去呢?自然是通過RawPostingList找到byteStart,然后根據byteStart在ByteBlockPool中找到docid+freq及prox信息的起始位置,從起始位置開始的兩個大小為5的塊,第一個就是docid+freq信息的源頭,第二個就是prox信息的源頭,如果源頭的塊中包含了所有的信息,讀出來就可以了,如果源頭的塊中有指針,則沿著指針尋找到下一個塊,從而可以找到所有的信息。

  • 下面舉一個實例來表明如果進行緩存管理的:

此例子中,準備添加三個文件:

file01: common common common common common term

file02: common common common common common term term

file03: term term term common common common common common

file04: term

(1) 添加第一篇文檔第一個common

  • 在CharBlockPool中分配6個char來存放"common"字符串
  • 在ByteBlockPool中分配兩個塊,每個塊大小為5,以16結束,第一個塊用來存放docid+freq信息,第二個塊用來存放prox信息。此時docid+freq信息沒有寫入,docid+freq信息總是在下一篇文檔的處理過程出現了"common"的時候方才寫入,因為當一篇文檔沒有處理完畢的時候,freq也即詞頻是無法知道的。而prox信息存放0,是因為第一個common的位置為0,但是此0應該左移一位,最后一位置0表示沒有payload存儲,因而0<<1 + 0 = 0。
  • 在IntBlockPool中分配兩個int,一個指向第0個位置,是因為當前沒有docid+freq信息寫入,第二個指向第6個位置,是因為第5個位置寫入了prox信息。所以IntBlockPool中存放的是下一個要寫入的位置。

?

(2) 添加第四個common

  • 在ByteBlockPool中,prox信息已經存放了4個,第一個0是代表第一個位置為0,后面不跟隨payload。第二個2表示,位置增量(差值原則)為1,后面不跟隨payload(或然跟隨原則),1<<1 + 0 =2。第三個第四個同第二個。

?

(3) 添加第五個common

  • ByteBlockPool中,存放prox信息的塊已經全部填滿,必須重新分配新的塊。
  • 新的塊層次為2,大小為14,在緩存的最后追加分配。
  • 原塊中連同結束位在內的四個byte作為指針(綠色部分),指向新的塊的其實地址,在此為10.
  • 被指針占用的結束位之前的三位移到新的塊中,也即6, 7, 8移到10, 11, 12處,13處是第五個common的prox信息。
  • 指針的值并不都是四個byte的最后一位,當緩存很大的時候,指針的值也會很大。比如指針出現[0, 0, 0, -56],最后一位為負,并不表示指向的負位置,而是最后一個byte的第一位為1,顯示為有符號數為負,-56的二進制是11001000,和前三個byte拼稱int,大小為200也即指向第200個位置。比如指針出現[0, 0, 1, 2],其轉換為二進制的int為100000010,大小為258,也即指向第258個位置。比如指針出現?
    [0, 0, 1, -98],轉換為二進制的int為110011110,大小為414,也即指向第414個位置。

?

(4) 添加第一篇文檔,第一個term

  • CharBlockPool中分配了5個char來存放"term"
  • ByteBlockPool中分配了兩個塊來分別存放docid+freq信息和prox信息。第一個塊沒有信息寫入,第二個塊寫入了"term"的位置信息,即出現在第5個位置,并且后面沒有payload,5<<1 + 0 = 10。
  • IntBlockPool中分配了兩個int來指向"term"的兩個塊中下一個寫入的位置。

?

(5) 添加第二篇文檔第一個common

  • 第一篇文檔的common的docid+freq信息寫入。在第一篇文檔中,"common"出現了5次,文檔號為0,按照或然跟隨原則,存放的信息為[docid<<1 + 0, 5] = [0, 5],docid左移一位,最后一位為0表示freq大于1。
  • 第二篇文檔第一個common的位置信息也寫入了,位置為0,0<<1 + 0 = 0。

?

(6) 添加第二篇文檔第一個term

  • 第一篇文檔中的term的docid+freq信息寫入,在第一篇文檔中,"term"出現了1次,文檔號為0,所以存儲信息為[docid<<1 + 1] = [1],文檔號左移一位,最后一位為1表示freq為1。
  • 第二篇文檔第一個term的prox信息也寫入了,在第5個位置,5<<1 + 0 = 10。
  • 第二篇文檔中的5個common的prox信息也寫入了,分別為從14到18的[0, 2, 2, 2, 2]

?

(7) 添加第三篇文檔的第一個term

  • 第二篇文檔的term的docid+freq信息寫入,在第二篇文檔中,文檔號為1,"term"出現了2次,所以存儲為[docid<<1 + 0, freq] = [2, 2],存儲在25, 26兩個位置。
  • 第二篇文檔中兩個term的位置信息也寫入了,為30, 31的[10, 2],也即出現在第5個,第6個位置,后面不跟隨payload。
  • 第三篇文檔的第一個term的位置信息也寫入了,在第0個位置,不跟payload,為32保存的[0]

?

(8) 添加第三篇文檔第二個term

  • term的位置信息已經填滿了,必須分配新的塊,層次為2,大小為14,結束符為17,也即圖中34到47的位置。
  • 30到33的四個byte組成一個int指針,指向第34個位置
  • 原來30到32的三個prox信息移到34到36的位置。
  • 在37處保存第三篇文檔第二個term的位置信息,位置為1,不跟隨payload,1<<1 + 0 = 2。

?

(9) 添加第三篇文檔第四個common

  • 第二篇文檔中"common"的docid+freq信息寫入,文檔號為1,出現了5次,存儲為[docid << 1 + 0, freq],docid取差值為1,因而存儲為 [2, 5],即2,3的位置。
  • 第三篇文檔中前四個common的位置信息寫入,即從19到22的[6, 2, 2, 2],即出現在第3個,第4個,第5個,第6個位置。
  • 第三篇文檔中的第三個"term"的位置信息也寫入,為38處的[2]。

?

(10) 添加第三篇文檔的第五個common

  • 雖然common已經分配了層次為2,大小為14的第二個塊(從10到23),不過還是用完了,需要在緩存的最后分配新的塊,層次為3,大小為20,結束符為18,也即從48到67的位置。
  • 從20到23的四個byte組成一個int指針指向新分配的塊。
  • 原來20到22的數據移到48至50的位置。
  • 第三篇文檔的第五個common的位置信息寫入,為第51個位置的[2],也即緊跟上個common,后面沒有payload信息。

?

(11) 添加第四篇文檔的第一個term

  • 寫入第三篇文檔的term的docid+freq信息,文檔號為2,出現了三次,存儲為[docid<<1+0, freq],docid取差值為1,因而存儲為[2, 3]。
  • 然而存儲term的docid+freq信息的塊已經滿了,需要在緩存的最后追加新的塊,層次為2,大小為14,結束符為17,即從68到81的位置。
  • 從25到28的四個byte組成一個int指針指向新分配的塊。
  • 原來25到26的信息移到68, 69處,在70, 71處寫入第三篇文檔的docid+freq信息[2, 3]

?

(12) 最終PostingList, CharBlockPool, IntBlockPool,ByteBlockPool的關系如下圖:



?

?


?

6、關閉IndexWriter對象

代碼:

writer.close();

--> IndexWriter.closeInternal(boolean)

????? --> (1) 將索引信息由內存寫入磁盤: flush(waitForMerges, true, true);?
????? --> (2) 進行段合并: mergeScheduler.merge(this);

對段的合并將在后面的章節進行討論,此處僅僅討論將索引信息由寫入磁盤的過程。

代碼:

IndexWriter.flush(boolean triggerMerge, boolean flushDocStores, boolean flushDeletes)

--> IndexWriter.doFlush(boolean flushDocStores, boolean flushDeletes)

????? --> IndexWriter.doFlushInternal(boolean flushDocStores, boolean flushDeletes)

將索引寫入磁盤包括以下幾個過程:

  • 得到要寫入的段名:String segment = docWriter.getSegment();
  • DocumentsWriter將緩存的信息寫入段:docWriter.flush(flushDocStores);
  • 生成新的段信息對象:newSegment = new SegmentInfo(segment, flushedDocCount, directory, false, true, docStoreOffset, docStoreSegment, docStoreIsCompoundFile, docWriter.hasProx());
  • 準備刪除文檔:docWriter.pushDeletes();
  • 生成cfs段:docWriter.createCompoundFile(segment);
  • 刪除文檔:applyDeletes();

6.1、得到要寫入的段名

代碼:

SegmentInfo newSegment = null;

final int numDocs = docWriter.getNumDocsInRAM();//文檔總數

String docStoreSegment = docWriter.getDocStoreSegment();//存儲域和詞向量所要要寫入的段名,"_0"???

int docStoreOffset = docWriter.getDocStoreOffset();//存儲域和詞向量要寫入的段中的偏移量

String segment = docWriter.getSegment();//段名,"_0"

在Lucene的索引文件結構一章做過詳細介紹,存儲域和詞向量可以和索引域存儲在不同的段中。

6.2、將緩存的內容寫入段

代碼:

flushedDocCount = docWriter.flush(flushDocStores);

此過程又包含以下兩個階段;

  • 按照基本索引鏈關閉存儲域和詞向量信息
  • 按照基本索引鏈的結構將索引結果寫入段

6.2.1、按照基本索引鏈關閉存儲域和詞向量信息

代碼為:

closeDocStore();

flushState.numDocsInStore = 0;

其主要是根據基本索引鏈結構,關閉存儲域和詞向量信息:

  • consumer(DocFieldProcessor).closeDocStore(flushState);
    • consumer(DocInverter).closeDocStore(state);
      • consumer(TermsHash).closeDocStore(state);
        • consumer(FreqProxTermsWriter).closeDocStore(state);
        • if (nextTermsHash != null) nextTermsHash.closeDocStore(state);
          • consumer(TermVectorsTermsWriter).closeDocStore(state);
      • endConsumer(NormsWriter).closeDocStore(state);
    • fieldsWriter(StoredFieldsWriter).closeDocStore(state);

其中有實質意義的是以下兩個closeDocStore:

  • 詞向量的關閉:TermVectorsTermsWriter.closeDocStore(SegmentWriteState)

void closeDocStore(final SegmentWriteState state) throws IOException {

???????????????????? if (tvx != null) {?
??????????? //為不保存詞向量的文檔在tvd文件中寫入零。即便不保存詞向量,在tvx, tvd中也保留一個位置?
??????????? fill(state.numDocsInStore - docWriter.getDocStoreOffset());?
??????????? //關閉tvx, tvf, tvd文件的寫入流?
??????????? tvx.close();?
??????????? tvf.close();?
??????????? tvd.close();?
??????????? tvx = null;?
??????????? //記錄寫入的文件名,為以后生成cfs文件的時候,將這些寫入的文件生成一個統一的cfs文件。?
??????????? state.flushedFiles.add(state.docStoreSegmentName + "." + IndexFileNames.VECTORS_INDEX_EXTENSION);?
??????????? state.flushedFiles.add(state.docStoreSegmentName + "." + IndexFileNames.VECTORS_FIELDS_EXTENSION);?
??????????? state.flushedFiles.add(state.docStoreSegmentName + "." + IndexFileNames.VECTORS_DOCUMENTS_EXTENSION);?
??????????? //從DocumentsWriter的成員變量openFiles中刪除,未來可能被IndexFileDeleter刪除?
??????????? docWriter.removeOpenFile(state.docStoreSegmentName + "." + IndexFileNames.VECTORS_INDEX_EXTENSION);?
??????????? docWriter.removeOpenFile(state.docStoreSegmentName + "." + IndexFileNames.VECTORS_FIELDS_EXTENSION);?
??????????? docWriter.removeOpenFile(state.docStoreSegmentName + "." + IndexFileNames.VECTORS_DOCUMENTS_EXTENSION);?
??????????? lastDocID = 0;?
??????? }?????
}
  • 存儲域的關閉:StoredFieldsWriter.closeDocStore(SegmentWriteState)

public void closeDocStore(SegmentWriteState state) throws IOException {

??? //關閉fdx, fdt寫入流

??? fieldsWriter.close();?
??? --> fieldsStream.close();?
??? --> indexStream.close();?
??? fieldsWriter = null;?
??? lastDocID = 0;

??? //記錄寫入的文件名?
??? state.flushedFiles.add(state.docStoreSegmentName + "." + IndexFileNames.FIELDS_EXTENSION);?
??? state.flushedFiles.add(state.docStoreSegmentName + "." + IndexFileNames.FIELDS_INDEX_EXTENSION);?
??? state.docWriter.removeOpenFile(state.docStoreSegmentName + "." + IndexFileNames.FIELDS_EXTENSION);?
??? state.docWriter.removeOpenFile(state.docStoreSegmentName + "." + IndexFileNames.FIELDS_INDEX_EXTENSION);?
}


6.2.2、按照基本索引鏈的結構將索引結果寫入段

代碼為:

consumer(DocFieldProcessor).flush(threads, flushState);

??? //回收fieldHash,以便用于下一輪的索引,為提高效率,索引鏈中的對象是被復用的。

??? Map> childThreadsAndFields = new HashMap>();?
??? for ( DocConsumerPerThread thread : threads) {?
??????? DocFieldProcessorPerThread perThread = (DocFieldProcessorPerThread) thread;?
??????? childThreadsAndFields.put(perThread.consumer, perThread.fields());?
????????perThread.trimFields(state);?
??? }

??? //寫入存儲域

??? --> fieldsWriter(StoredFieldsWriter).flush(state);

??? //寫入索引域

??? --> consumer(DocInverter).flush(childThreadsAndFields, state);

??? //寫入域元數據信息,并記錄寫入的文件名,以便以后生成cfs文件

??? --> final String fileName = state.segmentFileName(IndexFileNames.FIELD_INFOS_EXTENSION);

??? --> fieldInfos.write(state.directory, fileName);

??? --> state.flushedFiles.add(fileName);

此過程也是按照基本索引鏈來的:

  • consumer(DocFieldProcessor).flush(…);
    • consumer(DocInverter).flush(…);
      • consumer(TermsHash).flush(…);
        • consumer(FreqProxTermsWriter).flush(…);
        • if (nextTermsHash != null) nextTermsHash.flush(…);
          • consumer(TermVectorsTermsWriter).flush(…);
      • endConsumer(NormsWriter).flush(…);
    • fieldsWriter(StoredFieldsWriter).flush(…);

6.2.2.1、寫入存儲域

代碼為:

StoredFieldsWriter.flush(SegmentWriteState state) {?
??? if (state.numDocsInStore > 0) {?
????? initFieldsWriter();?
????? fill(state.numDocsInStore - docWriter.getDocStoreOffset());?
??? }?
??? if (fieldsWriter != null)?
????? fieldsWriter.flush();?
? }

從代碼中可以看出,是寫入fdx, fdt兩個文件,但是在上述的closeDocStore已經寫入了,并且把state.numDocsInStore置零,fieldsWriter設為null,在這里其實什么也不做。

6.2.2.2、寫入索引域

代碼為:

DocInverter.flush(Map>, SegmentWriteState)

??? //寫入倒排表及詞向量信息

??? --> consumer(TermsHash).flush(childThreadsAndFields, state);

??? //寫入標準化因子

??? --> endConsumer(NormsWriter).flush(endChildThreadsAndFields, state);

6.2.2.2.1、寫入倒排表及詞向量信息

代碼為:

TermsHash.flush(Map>, SegmentWriteState)

??? //寫入倒排表信息

??? --> consumer(FreqProxTermsWriter).flush(childThreadsAndFields, state);

?? //回收RawPostingList

??? --> shrinkFreePostings(threadsAndFields, state);

??? //寫入詞向量信息

??? --> if (nextTermsHash != null) nextTermsHash.flush(nextThreadsAndFields, state);

????????? --> consumer(TermVectorsTermsWriter).flush(childThreadsAndFields, state);

6.2.2.2.1.1、寫入倒排表信息

代碼為:

FreqProxTermsWriter.flush(Map?????????????????????????????????????? Collection>, SegmentWriteState)

????(a) 所有域按名稱排序,使得同名域能夠一起處理

??? Collections.sort(allFields);

??? final int numAllFields = allFields.size();

????(b) 生成倒排表的寫對象

??? final FormatPostingsFieldsConsumer consumer = new FormatPostingsFieldsWriter(state, fieldInfos);

??? int start = 0;

????(c) 對于每一個域

??? while(start < numAllFields) {

????????(c-1) 找出所有的同名域

??????? final FieldInfo fieldInfo = allFields.get(start).fieldInfo;

??????? final String fieldName = fieldInfo.name;

??????? int end = start+1;

??????? while(end < numAllFields && allFields.get(end).fieldInfo.name.equals(fieldName))

??????????? end++;

??????? FreqProxTermsWriterPerField[] fields = new FreqProxTermsWriterPerField[end-start];

??????? for(int i=start;i

??????????? fields[i-start] = allFields.get(i);

??????????? fieldInfo.storePayloads |= fields[i-start].hasPayloads;

??????? }

????????(c-2) 將同名域的倒排表添加到文件

??????? appendPostings(fields, consumer);

???????(c-3) 釋放空間

??????? for(int i=0;i

??????????? TermsHashPerField perField = fields[i].termsHashPerField;

??????????? int numPostings = perField.numPostings;

??????????? perField.reset();

??????????? perField.shrinkHash(numPostings);

??????????? fields[i].reset();

??????? }

??????? start = end;

??? }

????(d) 關閉倒排表的寫對象

??? consumer.finish();

(b) 生成倒排表的寫對象

代碼為:

public FormatPostingsFieldsWriter(SegmentWriteState state, FieldInfos fieldInfos) throws IOException {?
??? dir = state.directory;?
??? segment = state.segmentName;?
??? totalNumDocs = state.numDocs;?
??? this.fieldInfos = fieldInfos;?
??? //用于寫tii,tis?
??? termsOut = new TermInfosWriter(dir, segment, fieldInfos, state.termIndexInterval);?
??? //用于寫freq, prox的跳表??
??? skipListWriter = new DefaultSkipListWriter(termsOut.skipInterval, termsOut.maxSkipLevels, totalNumDocs, null, null);?
??? //記錄寫入的文件名,?
??? state.flushedFiles.add(state.segmentFileName(IndexFileNames.TERMS_EXTENSION));?
??? state.flushedFiles.add(state.segmentFileName(IndexFileNames.TERMS_INDEX_EXTENSION));??
??? //用以上兩個寫對象,按照一定的格式寫入段?
??? termsWriter = new FormatPostingsTermsWriter(state, this);?
}

對象結構如下:

consumer????FormatPostingsFieldsWriter? (id=119)? //用于處理一個域?
??? dir??? SimpleFSDirectory? (id=126)?? //目標索引文件夾?
??? totalNumDocs??? 8?? //文檔總數?
??? fieldInfos??? FieldInfos? (id=70)? //域元數據信息???
??? segment??? "_0"?? //段名?
??? skipListWriter??? DefaultSkipListWriter? (id=133)? //freq, prox中跳表的寫對象???
??? termsOut??? TermInfosWriter? (id=125)? //tii, tis文件的寫對象?
??? termsWriter????FormatPostingsTermsWriter? (id=135)? //用于添加詞(Term)?
??????? currentTerm??? null????
??????? currentTermStart??? 0????
??????? fieldInfo??? null????
??????? freqStart??? 0????
??????? proxStart??? 0????
??????? termBuffer??? null????
??????? termsOut??? TermInfosWriter? (id=125)????
??????? docsWriter????FormatPostingsDocsWriter? (id=139)? //用于寫入此詞的docid, freq信息?
??????????? df??? 0????
??????????? fieldInfo??? null????
??????????? freqStart??? 0????
??????????? lastDocID??? 0????
??????????? omitTermFreqAndPositions??? false????
??????????? out??? SimpleFSDirectory$SimpleFSIndexOutput? (id=144)????
??????????? skipInterval??? 16????
??????????? skipListWriter??? DefaultSkipListWriter? (id=133)????
??????????? storePayloads??? false????
??????????? termInfo??? TermInfo? (id=151)????
??????????? totalNumDocs??? 8?????
??????????? posWriter????FormatPostingsPositionsWriter? (id=146)? //用于寫入此詞在此文檔中的位置信息???
??????????????? lastPayloadLength??? -1????
??????????????? lastPosition??? 0????
??????????????? omitTermFreqAndPositions??? false????
??????????????? out??? SimpleFSDirectory$SimpleFSIndexOutput? (id=157)????
??????????????? parent??? FormatPostingsDocsWriter? (id=139)????
??????????????? storePayloads??? false???
  • FormatPostingsFieldsWriter.addField(FieldInfo field)用于添加索引域信息,其返回FormatPostingsTermsConsumer用于添加詞信息
  • FormatPostingsTermsConsumer.addTerm(char[] text, int start)用于添加詞信息,其返回FormatPostingsDocsConsumer用于添加freq信息
  • FormatPostingsDocsConsumer.addDoc(int docID, int termDocFreq)用于添加freq信息,其返回FormatPostingsPositionsConsumer用于添加prox信息
  • FormatPostingsPositionsConsumer.addPosition(int position, byte[] payload, int payloadOffset, int payloadLength)用于添加prox信息

(c-2) 將同名域的倒排表添加到文件

代碼為:

?

FreqProxTermsWriter.appendPostings(FreqProxTermsWriterPerField[], FormatPostingsFieldsConsumer) {

??? int numFields = fields.length;

??? final FreqProxFieldMergeState[] mergeStates = new FreqProxFieldMergeState[numFields];

??? for(int i=0;i

????? FreqProxFieldMergeState fms = mergeStates[i] = new FreqProxFieldMergeState(fields[i]);

????? boolean result = fms.nextTerm(); //對所有的域,取第一個詞(Term)

??? }

????(1) 添加此域,雖然有多個域,但是由于是同名域,只取第一個域的信息即可。返回的是用于添加此域中的詞的對象。

??? final FormatPostingsTermsConsumer termsConsumer = consumer.addField(fields[0].fieldInfo);

??? FreqProxFieldMergeState[] termStates = new FreqProxFieldMergeState[numFields];

??? final boolean currentFieldOmitTermFreqAndPositions = fields[0].fieldInfo.omitTermFreqAndPositions;

????(2) 此while循環是遍歷每一個尚有未處理的詞的域,依次按照詞典順序處理這些域所包含的詞。當一個域中的所有的詞都被處理過后,則numFields減一,并從mergeStates數組中移除此域。直到所有的域的所有的詞都處理完畢,方才退出此循環。

??? while(numFields > 0) {

???????(2-1) 找出所有域中按字典順序的下一個詞??赡芏鄠€同名域中,都包含同一個term,因而要遍歷所有的numFields,得到所有的域里的下一個詞,numToMerge即為有多少個域包含此詞。

????? termStates[0] = mergeStates[0];

????? int numToMerge = 1;

????? for(int i=1;i

??????? final char[] text = mergeStates[i].text;

??????? final int textOffset = mergeStates[i].textOffset;

??????? final int cmp = compareText(text, textOffset, termStates[0].text, termStates[0].textOffset);

??????? if (cmp < 0) {

????????? termStates[0] = mergeStates[i];

????????? numToMerge = 1;

??????? } else if (cmp == 0)

????????? termStates[numToMerge++] = mergeStates[i];

????? }

????? (2-2) 添加此詞,返回FormatPostingsDocsConsumer用于添加文檔號(doc ID)及詞頻信息(freq)

????? final FormatPostingsDocsConsumer docConsumer = termsConsumer.addTerm(termStates[0].text, termStates[0].textOffset);

??????(2-3) 由于共numToMerge個域都包含此詞,每個詞都有一個鏈表的文檔號表示包含這些詞的文檔。此循環遍歷所有的包含此詞的域,依次按照從小到大的循序添加包含此詞的文檔號及詞頻信息。當一個域中對此詞的所有文檔號都處理過了,則numToMerge減一,并從termStates數組中移除此域。當所有包含此詞的域的所有文檔號都處理過了,則結束此循環。

????? while(numToMerge > 0) {

????????(2-3-1) 找出最小的文檔號

??????? FreqProxFieldMergeState minState = termStates[0];

??????? for(int i=1;i

????????? if (termStates[i].docID < minState.docID)

??????????? minState = termStates[i];

??????? final int termDocFreq = minState.termFreq;

????????(2-3-2) 添加文檔號及詞頻信息,并形成跳表,返回FormatPostingsPositionsConsumer用于添加位置(prox)信息

??????? final FormatPostingsPositionsConsumer posConsumer = docConsumer.addDoc(minState.docID, termDocFreq);

????????//ByteSliceReader是用于讀取bytepool中的prox信息的。

??????? final ByteSliceReader prox = minState.prox;

??????? if (!currentFieldOmitTermFreqAndPositions) {

????????? int position = 0;

??????????(2-3-3) 此循環對包含此詞的文檔,添加位置信息

????????? for(int j=0;j

??????????? final int code = prox.readVInt();

??????????? position += code >> 1;

??????????? final int payloadLength;

????????????// 如果此位置有payload信息,則從bytepool中讀出,否則設為零。

??????????? if ((code & 1) != 0) {

????????????? payloadLength = prox.readVInt();

????????????? if (payloadBuffer == null || payloadBuffer.length < payloadLength)

??????????????? payloadBuffer = new byte[payloadLength];

????????????? prox.readBytes(payloadBuffer, 0, payloadLength);

??????????? } else

????????????? payloadLength = 0;

??????????????//添加位置(prox)信息

????????????? posConsumer.addPosition(position, payloadBuffer, 0, payloadLength);

????????? }

????????? posConsumer.finish();

??????? }

???????(2-3-4) 判斷退出條件,上次選中的域取得下一個文檔號,如果沒有,則說明此域包含此詞的文檔已經處理完畢,則從termStates中刪除此域,并將numToMerge減一。然后此域取得下一個詞,當循環到(2)的時候,表明此域已經開始處理下一個詞。如果沒有下一個詞,說明此域中的所有的詞都處理完畢,則從mergeStates中刪除此域,并將numFields減一,當numFields為0的時候,循環(2)也就結束了。

??????? if (!minState.nextDoc()) {//獲得下一個docid

????????? //如果此域包含此詞的文檔已經沒有下一篇docid,則從數組termStates中移除,numToMerge減一。

????????? int upto = 0;

????????? for(int i=0;i

??????????? if (termStates[i] != minState)

????????????? termStates[upto++] = termStates[i];

????????? numToMerge--;

????????? //此域則取下一個詞(term),在循環(2)處來參與下一個詞的合并

????????? if (!minState.nextTerm()) {

??????????? //如果此域沒有下一個詞了,則此域從數組mergeStates中移除,numFields減一。

??????????? upto = 0;

??????????? for(int i=0;i

????????????? if (mergeStates[i] != minState)

??????????????? mergeStates[upto++] = mergeStates[i];

??????????? numFields--;

????????? }

??????? }

????? }

??????(2-4) 經過上面的過程,docid和freq信息雖已經寫入段文件,而跳表信息并沒有寫到文件中,而是寫入skip buffer里面了,此處真正寫入文件。并且詞典(tii, tis)也應該寫入文件。

????? docConsumer(FormatPostingsDocsWriter).finish();

??? }

??? termsConsumer.finish();

? }

(2-3-4) 獲得下一篇文檔號代碼如下:

?

public boolean nextDoc() {//如何獲取下一個docid

? if (freq.eof()) {//如果bytepool中的freq信息已經讀完

??? if (p.lastDocCode != -1) {//由上述緩存管理,PostingList里面還存著最后一篇文檔的文檔號及詞頻信息,則將最后一篇文檔返回

????? docID = p.lastDocID;

????? if (!field.omitTermFreqAndPositions)

??????? termFreq = p.docFreq;

????? p.lastDocCode = -1;

????? return true;

??? } else

????? return false;//沒有下一篇文檔

? }

? final int code = freq.readVInt();//如果bytepool中的freq信息尚未讀完

? if (field.omitTermFreqAndPositions)

??? docID += code;

? else {

??? //讀出文檔號及詞頻信息。

??? docID += code >>> 1;

??? if ((code & 1) != 0)

????? termFreq = 1;

??? else

????? termFreq = freq.readVInt();

? }

? return true;

}

(2-3-2) 添加文檔號及詞頻信息代碼如下:

?

FormatPostingsPositionsConsumer FormatPostingsDocsWriter.addDoc(int docID, int termDocFreq) {

??? final int delta = docID - lastDocID;

??? //當文檔數量達到skipInterval倍數的時候,添加跳表項。

??? if ((++df % skipInterval) == 0) {

????? skipListWriter.setSkipData(lastDocID, storePayloads, posWriter.lastPayloadLength);

????? skipListWriter.bufferSkip(df);

?? }

?? lastDocID = docID;

?? if (omitTermFreqAndPositions)

???? out.writeVInt(delta);

?? else if (1 == termDocFreq)

???? out.writeVInt((delta<<1) | 1);

?? else {

???? //寫入文檔號及詞頻信息。

???? out.writeVInt(delta<<1);

???? out.writeVInt(termDocFreq);

?? }

?? return posWriter;

}

(2-3-3) 添加位置信息:

?

FormatPostingsPositionsWriter.addPosition(int position, byte[] payload, int payloadOffset, int payloadLength) {

??? final int delta = position - lastPosition;

??? lastPosition = position;

??? if (storePayloads) {

??????? //保存位置及payload信息

??????? if (payloadLength != lastPayloadLength) {

??????????? lastPayloadLength = payloadLength;

??????????? out.writeVInt((delta<<1)|1);

??????????? out.writeVInt(payloadLength);

??????? } else

??????????? out.writeVInt(delta << 1);

??????????? if (payloadLength > 0)

??????????????? out.writeBytes(payload, payloadLength);

??? } else

??????? out.writeVInt(delta);

}

(2-4) 將跳表和詞典(tii, tis)寫入文件

FormatPostingsDocsWriter.finish() {

??? //將跳表緩存寫入文件

??? long skipPointer = skipListWriter.writeSkip(out);

??? if (df > 0) {

????? //將詞典(terminfo)寫入tii,tis文件

????? parent.termsOut(TermInfosWriter).add(fieldInfo.number, utf8.result, utf8.length, termInfo);

??? }

? }

將跳表緩存寫入文件:

DefaultSkipListWriter(MultiLevelSkipListWriter).writeSkip(IndexOutput)? {

??? long skipPointer = output.getFilePointer();

??? if (skipBuffer == null || skipBuffer.length == 0) return skipPointer;

??? //正如我們在索引文件格式中分析的那樣, 高層在前,低層在后,除最低層外,其他的層都有長度保存。

??? for (int level = numberOfSkipLevels - 1; level > 0; level--) {

????? long length = skipBuffer[level].getFilePointer();

????? if (length > 0) {

??????? output.writeVLong(length);

??????? skipBuffer[level].writeTo(output);

????? }

??? }

??? //寫入最低層

??? skipBuffer[0].writeTo(output);

??? return skipPointer;

? }

將詞典(terminfo)寫入tii,tis文件:

  • tii文件是tis文件的類似跳表的東西,是在tis文件中每隔indexInterval個詞提取出一個詞放在tii文件中,以便很快的查找到詞。
  • 因而TermInfosWriter類型中有一個成員變量other也是TermInfosWriter類型的,還有一個成員變量isIndex來表示此對象是用來寫tii文件的還是用來寫tis文件的。
  • 如果一個TermInfosWriter對象的isIndex=false則,它是用來寫tis文件的,它的other指向的是用來寫tii文件的TermInfosWriter對象
  • 如果一個TermInfosWriter對象的isIndex=true則,它是用來寫tii文件的,它的other指向的是用來寫tis文件的TermInfosWriter對象

TermInfosWriter.add (int fieldNumber, byte[] termBytes, int termBytesLength, TermInfo ti) {

??? //如果詞的總數是indexInterval的倍數,則應該寫入tii文件

??? if (!isIndex && size % indexInterval == 0)

????? other.add(lastFieldNumber, lastTermBytes, lastTermBytesLength, lastTi);

??? //將詞寫入tis文件

??? writeTerm(fieldNumber, termBytes, termBytesLength);

??? output.writeVInt(ti.docFreq);?????????????????????? // write doc freq

??? output.writeVLong(ti.freqPointer - lastTi.freqPointer); // write pointers

??? output.writeVLong(ti.proxPointer - lastTi.proxPointer);

??? if (ti.docFreq >= skipInterval) {

????? output.writeVInt(ti.skipOffset);

??? }

??? if (isIndex) {

????? output.writeVLong(other.output.getFilePointer() - lastIndexPointer);

????? lastIndexPointer = other.output.getFilePointer(); // write pointer

??? }

??? lastFieldNumber = fieldNumber;

??? lastTi.set(ti);

??? size++;

? }

6.2.2.2.1.2、寫入詞向量信息

代碼為:

TermVectorsTermsWriter.flush (Map>?
????????????????????????????????????????????? threadsAndFields, final SegmentWriteState state) {

??? if (tvx != null) {

????? if (state.numDocsInStore > 0)

??????? fill(state.numDocsInStore - docWriter.getDocStoreOffset());

????? tvx.flush();

????? tvd.flush();

????? tvf.flush();

??? }

??? for (Map.Entry> entry :?
????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????? threadsAndFields.entrySet()) {

????? for (final TermsHashConsumerPerField field : entry.getValue() ) {

??????? TermVectorsTermsWriterPerField perField = (TermVectorsTermsWriterPerField) field;

??????? perField.termsHashPerField.reset();

??????? perField.shrinkHash();

????? }

????? TermVectorsTermsWriterPerThread perThread = (TermVectorsTermsWriterPerThread) entry.getKey();

????? perThread.termsHashPerThread.reset(true);

??? }

? }

從代碼中可以看出,是寫入tvx, tvd, tvf三個文件,但是在上述的closeDocStore已經寫入了,并且把tvx設為null,在這里其實什么也不做,僅僅是清空postingsHash,以便進行下一輪索引時重用此對象。

6.2.2.2.2、寫入標準化因子

代碼為:

NormsWriter.flush(Map> threadsAndFields,

?????????????????????????? SegmentWriteState state) {

??? final Map> byField = new HashMap>();

??? for (final Map.Entry> entry :?
?????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????? threadsAndFields.entrySet()) {

???? //遍歷所有的域,將同名域對應的NormsWriterPerField放到同一個鏈表中。

????? final Collection fields = entry.getValue();

????? final Iterator fieldsIt = fields.iterator();

????? while (fieldsIt.hasNext()) {

??????? final NormsWriterPerField perField = (NormsWriterPerField) fieldsIt.next();

??????? List l = byField.get(perField.fieldInfo);

??????? if (l == null) {

??????????? l = new ArrayList();

??????????? byField.put(perField.fieldInfo, l);

??????? }

??????? l.add(perField);

??? }

??? //記錄寫入的文件名,方便以后生成cfs文件。

??? final String normsFileName = state.segmentName + "." + IndexFileNames.NORMS_EXTENSION;

??? state.flushedFiles.add(normsFileName);

??? IndexOutput normsOut = state.directory.createOutput(normsFileName);

??? try {

????? //寫入nrm文件頭

????? normsOut.writeBytes(SegmentMerger.NORMS_HEADER, 0, SegmentMerger.NORMS_HEADER.length);

????? final int numField = fieldInfos.size();

????? int normCount = 0;

????? //對每一個域進行處理

????? for(int fieldNumber=0;fieldNumber

??????? final FieldInfo fieldInfo = fieldInfos.fieldInfo(fieldNumber);

??????? //得到同名域的鏈表

??????? List toMerge = byField.get(fieldInfo);

??????? int upto = 0;

??????? if (toMerge != null) {

????????? final int numFields = toMerge.size();

????????? normCount++;

????????? final NormsWriterPerField[] fields = new NormsWriterPerField[numFields];

????????? int[] uptos = new int[numFields];

????????? for(int j=0;j

??????????? fields[j] = toMerge.get(j);

????????? int numLeft = numFields;

????????? //處理同名的多個域

????????? while(numLeft > 0) {

??????????? //得到所有的同名域中最小的文檔號

??????????? int minLoc = 0;

??????????? int minDocID = fields[0].docIDs[uptos[0]];

??????????? for(int j=1;j

????????????? final int docID = fields[j].docIDs[uptos[j]];

????????????? if (docID < minDocID) {

??????????????? minDocID = docID;

??????????????? minLoc = j;

????????????? }

??????????? }

??????????? // 在nrm文件中,每一個文件都有一個位置,沒有設定的,放入默認值

??????????? for (;upto<minDocID;upto++)

????????????? normsOut.writeByte(defaultNorm);

??????????? //寫入當前的nrm值

??????????? normsOut.writeByte(fields[minLoc].norms[uptos[minLoc]]);

??????????? (uptos[minLoc])++;

??????????? upto++;

??????????? //如果當前域的文檔已經處理完畢,則numLeft減一,歸零時推出循環。

??????????? if (uptos[minLoc] == fields[minLoc].upto) {

????????????? fields[minLoc].reset();

????????????? if (minLoc != numLeft-1) {

??????????????? fields[minLoc] = fields[numLeft-1];

??????????????? uptos[minLoc] = uptos[numLeft-1];

????????????? }

????????????? numLeft--;

??????????? }

????????? }

????????? // 對所有的未設定nrm值的文檔寫入默認值。

????????? for(;upto

??????????? normsOut.writeByte(defaultNorm);

??????? } else if (fieldInfo.isIndexed && !fieldInfo.omitNorms) {

????????? normCount++;

????????? // Fill entire field with default norm:

????????? for(;upto

??????????? normsOut.writeByte(defaultNorm);

??????? }

????? }

??? } finally {

????? normsOut.close();

??? }

? }

6.2.2.3、寫入域元數據

代碼為:

FieldInfos.write(IndexOutput) {

??? output.writeVInt(CURRENT_FORMAT);

??? output.writeVInt(size());

??? for (int i = 0; i < size(); i++) {

????? FieldInfo fi = fieldInfo(i);

????? byte bits = 0x0;

????? if (fi.isIndexed) bits |= IS_INDEXED;

????? if (fi.storeTermVector) bits |= STORE_TERMVECTOR;

????? if (fi.storePositionWithTermVector) bits |= STORE_POSITIONS_WITH_TERMVECTOR;

????? if (fi.storeOffsetWithTermVector) bits |= STORE_OFFSET_WITH_TERMVECTOR;

????? if (fi.omitNorms) bits |= OMIT_NORMS;

????? if (fi.storePayloads) bits |= STORE_PAYLOADS;

????? if (fi.omitTermFreqAndPositions) bits |= OMIT_TERM_FREQ_AND_POSITIONS;

????? output.writeString(fi.name);

????? output.writeByte(bits);

??? }

}

此處基本就是按照fnm文件的格式寫入的。

6.3、生成新的段信息對象

代碼:

newSegment = new SegmentInfo(segment, flushedDocCount, directory, false, true, docStoreOffset, docStoreSegment, docStoreIsCompoundFile, docWriter.hasProx());

segmentInfos.add(newSegment);

?

6.4、準備刪除文檔

代碼:

docWriter.pushDeletes();

??? --> deletesFlushed.update(deletesInRAM);

此處將deletesInRAM全部加到deletesFlushed中,并把deletesInRAM清空。原因上面已經闡明。

6.5、生成cfs段

代碼:

docWriter.createCompoundFile(segment);

newSegment.setUseCompoundFile(true);

代碼為:

DocumentsWriter.createCompoundFile(String segment) {

??? CompoundFileWriter cfsWriter = new CompoundFileWriter(directory, segment + "." + IndexFileNames.COMPOUND_FILE_EXTENSION);

??? //將上述中記錄的文檔名全部加入cfs段的寫對象。

??? for (final String flushedFile : flushState.flushedFiles)

????? cfsWriter.addFile(flushedFile);

??? cfsWriter.close();

? }

6.6、刪除文檔

代碼:

applyDeletes();

代碼為:

boolean applyDeletes(SegmentInfos infos) {

? if (!hasDeletes())

??? return false;

? final int infosEnd = infos.size();

? int docStart = 0;

? boolean any = false;

? for (int i = 0; i < infosEnd; i++) {

??? assert infos.info(i).dir == directory;

??? SegmentReader reader = writer.readerPool.get(infos.info(i), false);

??? try {

????? any |= applyDeletes(reader, docStart);

????? docStart += reader.maxDoc();

??? } finally {

????? writer.readerPool.release(reader);

??? }

? }

? deletesFlushed.clear();

? return any;

}

  • Lucene刪除文檔可以用reader,也可以用writer,但是歸根結底還是用reader來刪除的。
  • reader的刪除有以下三種方式:
    • 按照詞刪除,刪除所有包含此詞的文檔。
    • 按照文檔號刪除。
    • 按照查詢對象刪除,刪除所有滿足此查詢的文檔。
  • 但是這三種方式歸根結底還是按照文檔號刪除,也就是寫.del文件的過程。

?

private final synchronized boolean applyDeletes(IndexReader reader, int docIDStart)

? throws CorruptIndexException, IOException {

? final int docEnd = docIDStart + reader.maxDoc();

? boolean any = false;

? //按照詞刪除,刪除所有包含此詞的文檔。

? TermDocs docs = reader.termDocs();

? try {

??? for (Entry entry: deletesFlushed.terms.entrySet()) {

????? Term term = entry.getKey();

????? docs.seek(term);

????? int limit = entry.getValue().getNum();

????? while (docs.next()) {

??????? int docID = docs.doc();

??????? if (docIDStart+docID >= limit)

????????? break;

??????? reader.deleteDocument(docID);

??????? any = true;

????? }

??? }

? } finally {

??? docs.close();

? }

? //按照文檔號刪除。

? for (Integer docIdInt : deletesFlushed.docIDs) {

??? int docID = docIdInt.intValue();

??? if (docID >= docIDStart && docID < docEnd) {

????? reader.deleteDocument(docID-docIDStart);

????? any = true;

??? }

? }

? //按照查詢對象刪除,刪除所有滿足此查詢的文檔。

? IndexSearcher searcher = new IndexSearcher(reader);

? for (Entry entry : deletesFlushed.queries.entrySet()) {

??? Query query = entry.getKey();

??? int limit = entry.getValue().intValue();

??? Weight weight = query.weight(searcher);

??? Scorer scorer = weight.scorer(reader, true, false);

??? if (scorer != null) {

????? while(true)? {

??????? int doc = scorer.nextDoc();

??????? if (((long) docIDStart) + doc >= limit)

????????? break;

??????? reader.deleteDocument(doc);

??????? any = true;

????? }

??? }

? }

? searcher.close();

? return any;

}


更多0

總結

以上是生活随笔為你收集整理的Lucene学习总结之四:Lucene索引过程分析的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

大地资源网第二页免费观看 | 亚洲大尺度无码无码专区 | 97久久超碰中文字幕 | 久久久中文久久久无码 | 欧美zoozzooz性欧美 | 国产麻豆精品一区二区三区v视界 | 人人妻人人澡人人爽精品欧美 | 国产莉萝无码av在线播放 | 疯狂三人交性欧美 | 免费人成在线视频无码 | 又大又硬又黄的免费视频 | 久久亚洲国产成人精品性色 | 久久www免费人成人片 | 国产精品办公室沙发 | 久久无码中文字幕免费影院蜜桃 | 亚洲国产精品久久人人爱 | 欧洲精品码一区二区三区免费看 | 国产suv精品一区二区五 | 麻豆av传媒蜜桃天美传媒 | 国产三级久久久精品麻豆三级 | 亚洲国产精品久久人人爱 | 亚洲欧美中文字幕5发布 | 中文无码精品a∨在线观看不卡 | 性史性农村dvd毛片 | 男人扒开女人内裤强吻桶进去 | 欧美一区二区三区 | 国内综合精品午夜久久资源 | 中文字幕无码热在线视频 | 精品国产一区二区三区av 性色 | 亚洲一区二区三区偷拍女厕 | 日韩 欧美 动漫 国产 制服 | 亚洲成在人网站无码天堂 | 亚洲熟熟妇xxxx | 色老头在线一区二区三区 | 任你躁国产自任一区二区三区 | 午夜成人1000部免费视频 | 丰满少妇高潮惨叫视频 | 国产亚洲人成在线播放 | 人人妻在人人 | 成人免费视频视频在线观看 免费 | 久久久久se色偷偷亚洲精品av | 天堂一区人妻无码 | 国产手机在线αⅴ片无码观看 | 国产亚洲精品久久久久久久久动漫 | 中文字幕av无码一区二区三区电影 | 2020久久香蕉国产线看观看 | 精品人妻人人做人人爽 | 最近免费中文字幕中文高清百度 | 日本www一道久久久免费榴莲 | 国产97在线 | 亚洲 | 欧洲欧美人成视频在线 | 精品国精品国产自在久国产87 | 天天拍夜夜添久久精品 | 日本xxxx色视频在线观看免费 | 清纯唯美经典一区二区 | 亚洲中文字幕无码一久久区 | 欧美高清在线精品一区 | 午夜福利不卡在线视频 | 300部国产真实乱 | 少妇无码一区二区二三区 | 狠狠色噜噜狠狠狠狠7777米奇 | 黑人巨大精品欧美黑寡妇 | 任你躁国产自任一区二区三区 | 欧美国产日产一区二区 | 日本一本二本三区免费 | 少妇被粗大的猛进出69影院 | 日本乱偷人妻中文字幕 | 图片小说视频一区二区 | 四虎国产精品免费久久 | 漂亮人妻洗澡被公强 日日躁 | 久久国产自偷自偷免费一区调 | 亚洲自偷精品视频自拍 | 兔费看少妇性l交大片免费 | 国产在线精品一区二区高清不卡 | 无码av免费一区二区三区试看 | 久久国产36精品色熟妇 | 国产精品va在线观看无码 | 国内揄拍国内精品少妇国语 | 在线观看欧美一区二区三区 | 亚洲乱码日产精品bd | www国产亚洲精品久久网站 | 婷婷五月综合缴情在线视频 | 无码免费一区二区三区 | 小鲜肉自慰网站xnxx | 中文精品无码中文字幕无码专区 | 国产性生交xxxxx无码 | 人人爽人人澡人人人妻 | 久久久亚洲欧洲日产国码αv | 天天拍夜夜添久久精品大 | 日本又色又爽又黄的a片18禁 | 午夜男女很黄的视频 | 亚洲色欲色欲欲www在线 | 精品国产aⅴ无码一区二区 | 国产女主播喷水视频在线观看 | 亚洲一区二区三区国产精华液 | 毛片内射-百度 | 亚洲欧美精品aaaaaa片 | 国产精品亚洲专区无码不卡 | 国产亚洲人成a在线v网站 | 中文字幕日产无线码一区 | 99久久久无码国产精品免费 | 无码人妻出轨黑人中文字幕 | 风流少妇按摩来高潮 | 免费视频欧美无人区码 | 2020最新国产自产精品 | 老子影院午夜精品无码 | 一个人免费观看的www视频 | 国产农村妇女aaaaa视频 撕开奶罩揉吮奶头视频 | 国产成人无码av片在线观看不卡 | 色婷婷久久一区二区三区麻豆 | 久久久久久久女国产乱让韩 | 国产精品亚洲综合色区韩国 | 国产suv精品一区二区五 | 女人和拘做爰正片视频 | 日韩精品乱码av一区二区 | 强开小婷嫩苞又嫩又紧视频 | 激情内射亚州一区二区三区爱妻 | 国产精品办公室沙发 | 国产人妖乱国产精品人妖 | 日本熟妇大屁股人妻 | 国内精品人妻无码久久久影院 | 欧美性猛交xxxx富婆 | 中文字幕乱码人妻二区三区 | 九月婷婷人人澡人人添人人爽 | 久青草影院在线观看国产 | 精品人妻av区 | 帮老师解开蕾丝奶罩吸乳网站 | 久久久久久久女国产乱让韩 | 狠狠cao日日穞夜夜穞av | 99国产欧美久久久精品 | 日韩无套无码精品 | 纯爱无遮挡h肉动漫在线播放 | 免费看男女做好爽好硬视频 | 任你躁在线精品免费 | 狂野欧美性猛xxxx乱大交 | 精品国产乱码久久久久乱码 | 在线精品亚洲一区二区 | 十八禁真人啪啪免费网站 | 国产美女极度色诱视频www | 又粗又大又硬又长又爽 | 日韩人妻系列无码专区 | 久久综合给久久狠狠97色 | 九九在线中文字幕无码 | 亚洲色在线无码国产精品不卡 | 波多野结衣av一区二区全免费观看 | 国产极品美女高潮无套在线观看 | 国产高清av在线播放 | 少妇一晚三次一区二区三区 | 中文字幕 亚洲精品 第1页 | 激情人妻另类人妻伦 | 青青草原综合久久大伊人精品 | 真人与拘做受免费视频一 | 久久视频在线观看精品 | 成人免费视频视频在线观看 免费 | 欧美日韩亚洲国产精品 | 亚洲色大成网站www | 欧美怡红院免费全部视频 | 久久久久久久人妻无码中文字幕爆 | 日本在线高清不卡免费播放 | 国产明星裸体无码xxxx视频 | 亚洲精品久久久久久一区二区 | 欧美成人家庭影院 | 丰满妇女强制高潮18xxxx | 人人妻人人澡人人爽欧美一区九九 | 人妻少妇精品视频专区 | 久久精品成人欧美大片 | 丰满妇女强制高潮18xxxx | 欧美老人巨大xxxx做受 | 麻豆成人精品国产免费 | 国产精品久久久久久久9999 | 又大又硬又爽免费视频 | 奇米影视888欧美在线观看 | 久久99久久99精品中文字幕 | 精品厕所偷拍各类美女tp嘘嘘 | 亚洲精品一区二区三区在线 | 偷窥日本少妇撒尿chinese | 男女猛烈xx00免费视频试看 | 暴力强奷在线播放无码 | 精品无码一区二区三区的天堂 | 草草网站影院白丝内射 | 国产在线无码精品电影网 | 亚洲国产午夜精品理论片 | 激情五月综合色婷婷一区二区 | 久久亚洲精品中文字幕无男同 | 国产成人一区二区三区在线观看 | 人妻人人添人妻人人爱 | 天天拍夜夜添久久精品大 | 无码av最新清无码专区吞精 | 亚洲一区二区三区播放 | 国产在线精品一区二区三区直播 | 鲁鲁鲁爽爽爽在线视频观看 | 国产成人无码a区在线观看视频app | 久久国产精品精品国产色婷婷 | 67194成是人免费无码 | 波多野结衣av在线观看 | 精品无码一区二区三区爱欲 | 亚洲精品综合五月久久小说 | 亚洲综合色区中文字幕 | 99久久无码一区人妻 | 丰满少妇弄高潮了www | 一本加勒比波多野结衣 | 最新国产乱人伦偷精品免费网站 | 对白脏话肉麻粗话av | 秋霞成人午夜鲁丝一区二区三区 | 婷婷丁香六月激情综合啪 | 精品一区二区三区波多野结衣 | 久久97精品久久久久久久不卡 | 国产区女主播在线观看 | v一区无码内射国产 | 无码成人精品区在线观看 | 人妻少妇精品视频专区 | 亚洲国产欧美国产综合一区 | 天海翼激烈高潮到腰振不止 | 欧美激情一区二区三区成人 | v一区无码内射国产 | 国内综合精品午夜久久资源 | 思思久久99热只有频精品66 | 成人精品天堂一区二区三区 | 国产乱人伦av在线无码 | 熟女少妇人妻中文字幕 | 国产三级久久久精品麻豆三级 | 任你躁在线精品免费 | 午夜肉伦伦影院 | 丝袜 中出 制服 人妻 美腿 | 国产精品人人妻人人爽 | 久久国产36精品色熟妇 | 永久免费观看美女裸体的网站 | 久久人人爽人人爽人人片av高清 | 国产在热线精品视频 | 大地资源中文第3页 | 又湿又紧又大又爽a视频国产 | 成 人 免费观看网站 | 无码一区二区三区在线观看 | 伊人久久大香线蕉亚洲 | 亚洲色欲色欲欲www在线 | 无套内谢的新婚少妇国语播放 | 亚洲码国产精品高潮在线 | 久久人人97超碰a片精品 | 国产做国产爱免费视频 | 精品国偷自产在线 | 成人免费视频在线观看 | 欧美日韩在线亚洲综合国产人 | 一区二区传媒有限公司 | 日产国产精品亚洲系列 | 奇米影视7777久久精品人人爽 | 女人被男人躁得好爽免费视频 | 欧美肥老太牲交大战 | 一二三四社区在线中文视频 | 无码国产色欲xxxxx视频 | 人人妻人人澡人人爽精品欧美 | 无码乱肉视频免费大全合集 | a片在线免费观看 | 2020久久香蕉国产线看观看 | 亚洲中文字幕在线无码一区二区 | 亚洲第一网站男人都懂 | 亚洲天堂2017无码中文 | 少妇性荡欲午夜性开放视频剧场 | 免费无码av一区二区 | 国产手机在线αⅴ片无码观看 | 久激情内射婷内射蜜桃人妖 | 亚洲熟妇色xxxxx欧美老妇y | 国产午夜手机精彩视频 | 东北女人啪啪对白 | 色婷婷香蕉在线一区二区 | 人妻少妇精品无码专区动漫 | 免费看男女做好爽好硬视频 | 丁香花在线影院观看在线播放 | 中文久久乱码一区二区 | 国产精品无码成人午夜电影 | 欧美肥老太牲交大战 | 无码av免费一区二区三区试看 | 亚洲国产午夜精品理论片 | 久久无码中文字幕免费影院蜜桃 | 性色av无码免费一区二区三区 | 成 人 网 站国产免费观看 | 日韩人妻无码一区二区三区久久99 | 国产av久久久久精东av | 熟妇女人妻丰满少妇中文字幕 | 久久国产精品精品国产色婷婷 | 激情五月综合色婷婷一区二区 | 天堂无码人妻精品一区二区三区 | 久久久久久亚洲精品a片成人 | 少妇人妻偷人精品无码视频 | 国产成人精品无码播放 | 亚洲小说春色综合另类 | 欧美猛少妇色xxxxx | 亚洲国产成人av在线观看 | 中文字幕无码免费久久9一区9 | 国产内射老熟女aaaa | 水蜜桃亚洲一二三四在线 | 国产在热线精品视频 | 天堂一区人妻无码 | 精品人人妻人人澡人人爽人人 | 国产精品理论片在线观看 | 成人三级无码视频在线观看 | 色欲av亚洲一区无码少妇 | 国产内射老熟女aaaa | 久久久久99精品国产片 | 无码精品国产va在线观看dvd | 狠狠色噜噜狠狠狠狠7777米奇 | 精品国产一区二区三区av 性色 | 骚片av蜜桃精品一区 | 久久综合给久久狠狠97色 | 国产精品久久久av久久久 | 国产综合色产在线精品 | 国模大胆一区二区三区 | 西西人体www44rt大胆高清 | 日本精品高清一区二区 | 99久久99久久免费精品蜜桃 | 红桃av一区二区三区在线无码av | 欧美真人作爱免费视频 | 亚洲国产精品成人久久蜜臀 | 黑人玩弄人妻中文在线 | 55夜色66夜色国产精品视频 | 国产av剧情md精品麻豆 | 一本色道久久综合亚洲精品不卡 | 日日干夜夜干 | 色一情一乱一伦一区二区三欧美 | 日韩精品无码一区二区中文字幕 | 国产成人精品必看 | 色欲人妻aaaaaaa无码 | 精品国产精品久久一区免费式 | 亚洲综合精品香蕉久久网 | 中文字幕人妻无码一区二区三区 | 国产suv精品一区二区五 | 欧美喷潮久久久xxxxx | 性欧美熟妇videofreesex | 日韩人妻无码中文字幕视频 | 99精品视频在线观看免费 | 久久国产精品精品国产色婷婷 | 夜夜躁日日躁狠狠久久av | 鲁鲁鲁爽爽爽在线视频观看 | 亚洲人成网站免费播放 | 天天躁日日躁狠狠躁免费麻豆 | 欧美性猛交xxxx富婆 | 无码福利日韩神码福利片 | 久久久久se色偷偷亚洲精品av | 少妇性l交大片欧洲热妇乱xxx | 久久久精品人妻久久影视 | 嫩b人妻精品一区二区三区 | 妺妺窝人体色www婷婷 | 狠狠亚洲超碰狼人久久 | 国产真实乱对白精彩久久 | 亚洲综合在线一区二区三区 | 欧洲精品码一区二区三区免费看 | 亚洲国产精品久久久天堂 | 啦啦啦www在线观看免费视频 | 综合人妻久久一区二区精品 | 牲欲强的熟妇农村老妇女视频 | 国产无遮挡又黄又爽又色 | 人妻熟女一区 | 最近免费中文字幕中文高清百度 | 久久 国产 尿 小便 嘘嘘 | 欧美人与动性行为视频 | 好屌草这里只有精品 | 真人与拘做受免费视频 | 色综合久久久久综合一本到桃花网 | 亚洲s色大片在线观看 | 国产午夜无码精品免费看 | 久久精品国产99久久6动漫 | 亚洲s码欧洲m码国产av | 捆绑白丝粉色jk震动捧喷白浆 | 亚洲码国产精品高潮在线 | 女人和拘做爰正片视频 | 色五月五月丁香亚洲综合网 | 国产精品沙发午睡系列 | 红桃av一区二区三区在线无码av | 国产午夜视频在线观看 | 性生交大片免费看女人按摩摩 | 国产内射老熟女aaaa | 四虎永久在线精品免费网址 | 国产精品人人爽人人做我的可爱 | 久久久中文久久久无码 | 精品无码一区二区三区的天堂 | 99久久99久久免费精品蜜桃 | 国色天香社区在线视频 | 成人免费无码大片a毛片 | 久久无码人妻影院 | 中文字幕 亚洲精品 第1页 | 欧美zoozzooz性欧美 | 日本精品少妇一区二区三区 | 中文无码伦av中文字幕 | 欧美精品一区二区精品久久 | 无码帝国www无码专区色综合 | 在线亚洲高清揄拍自拍一品区 | 亚洲国产精品一区二区美利坚 | 九九久久精品国产免费看小说 | 精品久久久无码中文字幕 | 欧美性生交xxxxx久久久 | 国产三级精品三级男人的天堂 | 国产色精品久久人妻 | 丰满诱人的人妻3 | 日本熟妇乱子伦xxxx | 日本www一道久久久免费榴莲 | 国产乱人伦av在线无码 | 色综合久久久无码网中文 | 日本护士xxxxhd少妇 | 亚洲中文字幕成人无码 | 黑人玩弄人妻中文在线 | 免费观看又污又黄的网站 | 激情亚洲一区国产精品 | 人人爽人人爽人人片av亚洲 | 国产av久久久久精东av | 国产乱人伦av在线无码 | 国产综合在线观看 | 欧美成人高清在线播放 | 国产成人av免费观看 | 久久久久国色av免费观看性色 | 国产精品嫩草久久久久 | 影音先锋中文字幕无码 | 熟女少妇在线视频播放 | 久久精品99久久香蕉国产色戒 | 色爱情人网站 | 搡女人真爽免费视频大全 | 国产97色在线 | 免 | 亚洲の无码国产の无码步美 | 亚洲狠狠婷婷综合久久 | 中文字幕av无码一区二区三区电影 | 又湿又紧又大又爽a视频国产 | 欧美人与动性行为视频 | 久久精品人人做人人综合 | 99riav国产精品视频 | 欧美丰满老熟妇xxxxx性 | 国产亚洲视频中文字幕97精品 | 国产精品资源一区二区 | 亚洲经典千人经典日产 | 国产亚洲精品久久久久久久久动漫 | 国产97在线 | 亚洲 | 国产成人无码av一区二区 | 国产免费久久精品国产传媒 | 狂野欧美性猛交免费视频 | 天天摸天天透天天添 | 无码毛片视频一区二区本码 | 亚洲性无码av中文字幕 | 最近的中文字幕在线看视频 | а√天堂www在线天堂小说 | 激情内射亚州一区二区三区爱妻 | 日日麻批免费40分钟无码 | 久久综合香蕉国产蜜臀av | 丰满人妻翻云覆雨呻吟视频 | 狠狠色丁香久久婷婷综合五月 | 好屌草这里只有精品 | 国产真人无遮挡作爱免费视频 | 3d动漫精品啪啪一区二区中 | 未满成年国产在线观看 | 亚洲aⅴ无码成人网站国产app | 人人妻人人澡人人爽人人精品 | www国产亚洲精品久久网站 | 精品无码国产自产拍在线观看蜜 | 又大又黄又粗又爽的免费视频 | 日本精品人妻无码免费大全 | 中文字幕无码乱人伦 | 国产日产欧产精品精品app | 午夜免费福利小电影 | 亚洲国产精品美女久久久久 | 无码av中文字幕免费放 | 亚洲日本va午夜在线电影 | 中文精品久久久久人妻不卡 | 国产乱人伦av在线无码 | www一区二区www免费 | 中文无码成人免费视频在线观看 | 国产肉丝袜在线观看 | 东北女人啪啪对白 | 精品欧洲av无码一区二区三区 | 人妻互换免费中文字幕 | 国产乱人无码伦av在线a | 荫蒂被男人添的好舒服爽免费视频 | 在线观看欧美一区二区三区 | 精品夜夜澡人妻无码av蜜桃 | 人人澡人人妻人人爽人人蜜桃 | 亚洲 a v无 码免 费 成 人 a v | 99久久久无码国产aaa精品 | 久久久久国色av免费观看性色 | 中文亚洲成a人片在线观看 | 国模大胆一区二区三区 | 国语精品一区二区三区 | 99久久人妻精品免费二区 | 九九综合va免费看 | 国内揄拍国内精品少妇国语 | 欧洲熟妇色 欧美 | 一本久久a久久精品亚洲 | 无码人妻丰满熟妇区五十路百度 | 日日麻批免费40分钟无码 | 图片区 小说区 区 亚洲五月 | 性色欲网站人妻丰满中文久久不卡 | 日本丰满熟妇videos | 亚洲一区二区三区国产精华液 | 曰本女人与公拘交酡免费视频 | 亚洲国产精品成人久久蜜臀 | 日本精品少妇一区二区三区 | 亚洲精品鲁一鲁一区二区三区 | 国产精品18久久久久久麻辣 | 国产av无码专区亚洲a∨毛片 | 国产激情无码一区二区app | 欧美日本精品一区二区三区 | 国产热a欧美热a在线视频 | 欧美猛少妇色xxxxx | 在线视频网站www色 | 台湾无码一区二区 | 免费观看黄网站 | 狠狠色噜噜狠狠狠狠7777米奇 | 中文字幕无码热在线视频 | 日日天日日夜日日摸 | 又黄又爽又色的视频 | 未满小14洗澡无码视频网站 | 国内精品人妻无码久久久影院蜜桃 | 亚洲第一无码av无码专区 | 久久无码人妻影院 | 亚洲色无码一区二区三区 | 人人妻人人澡人人爽欧美一区 | 亚洲小说春色综合另类 | 精品亚洲韩国一区二区三区 | 国产成人一区二区三区在线观看 | 国产亚洲视频中文字幕97精品 | 综合激情五月综合激情五月激情1 | 精品久久久久久亚洲精品 | 日日天日日夜日日摸 | 国产精品第一国产精品 | 国产熟妇高潮叫床视频播放 | 精品厕所偷拍各类美女tp嘘嘘 | 日本乱人伦片中文三区 | 青青久在线视频免费观看 | 亚洲最大成人网站 | 一个人免费观看的www视频 | 性生交大片免费看女人按摩摩 | 国产精品a成v人在线播放 | 最新国产麻豆aⅴ精品无码 | 老熟妇乱子伦牲交视频 | 性开放的女人aaa片 | 人妻少妇精品无码专区动漫 | 久久亚洲中文字幕精品一区 | 青草青草久热国产精品 | ass日本丰满熟妇pics | 日韩欧美中文字幕公布 | 图片区 小说区 区 亚洲五月 | 国产精品人人爽人人做我的可爱 | 久精品国产欧美亚洲色aⅴ大片 | 性色欲网站人妻丰满中文久久不卡 | 久久久成人毛片无码 | 免费视频欧美无人区码 | 欧美日韩视频无码一区二区三 | 欧美日本精品一区二区三区 | 四十如虎的丰满熟妇啪啪 | 大肉大捧一进一出视频出来呀 | 99久久精品日本一区二区免费 | 日日鲁鲁鲁夜夜爽爽狠狠 | 疯狂三人交性欧美 | 大色综合色综合网站 | 少妇厨房愉情理9仑片视频 | 黑人玩弄人妻中文在线 | 两性色午夜视频免费播放 | 国产精品怡红院永久免费 | 亚无码乱人伦一区二区 | 疯狂三人交性欧美 | 亚洲の无码国产の无码影院 | 国产舌乚八伦偷品w中 | 久久精品人人做人人综合 | 国产精品无码一区二区桃花视频 | 亚洲精品成人av在线 | 51国偷自产一区二区三区 | 国产真实伦对白全集 | 乱人伦人妻中文字幕无码久久网 | 无码国产激情在线观看 | 午夜性刺激在线视频免费 | 久久综合网欧美色妞网 | 欧美老妇交乱视频在线观看 | 国产偷国产偷精品高清尤物 | 无码av免费一区二区三区试看 | 日本乱人伦片中文三区 | 综合网日日天干夜夜久久 | av无码电影一区二区三区 | 又大又硬又黄的免费视频 | 性色av无码免费一区二区三区 | 久久99精品国产麻豆 | 亚洲欧美精品伊人久久 | 无码中文字幕色专区 | 精品国产乱码久久久久乱码 | 无码人妻久久一区二区三区不卡 | 搡女人真爽免费视频大全 | av无码不卡在线观看免费 | 嫩b人妻精品一区二区三区 | 国产两女互慰高潮视频在线观看 | 日韩欧美成人免费观看 | 午夜熟女插插xx免费视频 | 日韩人妻无码中文字幕视频 | 国产无av码在线观看 | 99久久久无码国产aaa精品 | a片免费视频在线观看 | 精品成人av一区二区三区 | 香蕉久久久久久av成人 | 欧美肥老太牲交大战 | 中国女人内谢69xxxxxa片 | 国语精品一区二区三区 | 天天躁日日躁狠狠躁免费麻豆 | 亚洲码国产精品高潮在线 | 欧美日韩久久久精品a片 | 久久久亚洲欧洲日产国码αv | 国产精品亚洲一区二区三区喷水 | 少妇的肉体aa片免费 | 精品无人国产偷自产在线 | 国产精品办公室沙发 | 无码乱肉视频免费大全合集 | 日韩亚洲欧美精品综合 | 国产一区二区不卡老阿姨 | 色五月五月丁香亚洲综合网 | 国产午夜亚洲精品不卡 | 小sao货水好多真紧h无码视频 | 成人影院yy111111在线观看 | 国产精品无码永久免费888 | 桃花色综合影院 | 人人爽人人澡人人高潮 | 中文字幕乱码人妻无码久久 | 精品日本一区二区三区在线观看 | 日本一卡二卡不卡视频查询 | 国产av一区二区精品久久凹凸 | 亚洲区欧美区综合区自拍区 | 日韩少妇白浆无码系列 | a国产一区二区免费入口 | 国产精品欧美成人 | 国内揄拍国内精品人妻 | 日欧一片内射va在线影院 | 一个人看的www免费视频在线观看 | 男女猛烈xx00免费视频试看 | 亚洲成a人片在线观看日本 | 黄网在线观看免费网站 | 7777奇米四色成人眼影 | 国产精品久久久 | 无码人妻精品一区二区三区下载 | 日本丰满护士爆乳xxxx | 久久久久久久人妻无码中文字幕爆 | 国产婷婷色一区二区三区在线 | 人妻人人添人妻人人爱 | 性欧美疯狂xxxxbbbb | 中文字幕乱码人妻二区三区 | 亚洲aⅴ无码成人网站国产app | 亚洲无人区一区二区三区 | 老司机亚洲精品影院无码 | 扒开双腿吃奶呻吟做受视频 | 大肉大捧一进一出视频出来呀 | 日产精品高潮呻吟av久久 | 成人三级无码视频在线观看 | 免费看少妇作爱视频 | 亚洲の无码国产の无码影院 | 亚洲第一网站男人都懂 | 欧美精品国产综合久久 | 丰满少妇人妻久久久久久 | 熟女少妇在线视频播放 | 无码国产乱人伦偷精品视频 | 精品无码一区二区三区的天堂 | 中文字幕无码免费久久9一区9 | 亚洲精品午夜国产va久久成人 | 精品久久久久久亚洲精品 | 婷婷丁香六月激情综合啪 | 欧美性猛交内射兽交老熟妇 | 蜜桃视频插满18在线观看 | 久久亚洲精品成人无码 | 国产麻豆精品一区二区三区v视界 | 亚洲精品久久久久久一区二区 | 亚洲s色大片在线观看 | www一区二区www免费 | 蜜桃臀无码内射一区二区三区 | 日本va欧美va欧美va精品 | 野狼第一精品社区 | 亚洲中文无码av永久不收费 | 国产性生大片免费观看性 | 内射白嫩少妇超碰 | 台湾无码一区二区 | 国产人成高清在线视频99最全资源 | 人人爽人人爽人人片av亚洲 | 国产午夜亚洲精品不卡下载 | 人妻尝试又大又粗久久 | 精品亚洲韩国一区二区三区 | 人妻夜夜爽天天爽三区 | 美女张开腿让人桶 | 久久zyz资源站无码中文动漫 | 色综合久久久久综合一本到桃花网 | 中文精品无码中文字幕无码专区 | 国内揄拍国内精品少妇国语 | 久久精品视频在线看15 | 国产亚洲人成a在线v网站 | 色老头在线一区二区三区 | 国产在线精品一区二区三区直播 | 无码人妻av免费一区二区三区 | 蜜桃av蜜臀av色欲av麻 999久久久国产精品消防器材 | 女人被爽到呻吟gif动态图视看 | 精品久久久中文字幕人妻 | 国产成人无码a区在线观看视频app | 无码精品国产va在线观看dvd | 国产真实乱对白精彩久久 | 国产两女互慰高潮视频在线观看 | 无码人妻少妇伦在线电影 | 中文无码精品a∨在线观看不卡 | 精品欧洲av无码一区二区三区 | 久久精品人妻少妇一区二区三区 | 国产又粗又硬又大爽黄老大爷视 | 欧美怡红院免费全部视频 | 国产精品成人av在线观看 | 少妇一晚三次一区二区三区 | 亚洲国产精品久久人人爱 | 性色av无码免费一区二区三区 | 国产一精品一av一免费 | 久久精品国产99久久6动漫 | 亚洲一区二区三区在线观看网站 | 人人妻人人澡人人爽欧美一区九九 | 亚洲精品久久久久久久久久久 | 亚洲色欲久久久综合网东京热 | 国产精品无码成人午夜电影 | 澳门永久av免费网站 | 亚洲精品久久久久久一区二区 | 欧美丰满熟妇xxxx性ppx人交 | 131美女爱做视频 | 国产精品理论片在线观看 | 丰满人妻被黑人猛烈进入 | 色噜噜亚洲男人的天堂 | 夜夜躁日日躁狠狠久久av | 欧美日韩一区二区免费视频 | 在线观看国产一区二区三区 | 亚洲精品无码国产 | 国产人成高清在线视频99最全资源 | 亚洲 另类 在线 欧美 制服 | 又粗又大又硬又长又爽 | 领导边摸边吃奶边做爽在线观看 | 天天摸天天透天天添 | 一个人看的www免费视频在线观看 | 青草视频在线播放 | 国产精品18久久久久久麻辣 | 国产特级毛片aaaaaa高潮流水 | 日韩成人一区二区三区在线观看 | av无码不卡在线观看免费 | 免费人成网站视频在线观看 | 国产综合久久久久鬼色 | 人妻互换免费中文字幕 | 国产成人无码区免费内射一片色欲 | 少妇人妻偷人精品无码视频 | 丰满少妇弄高潮了www | 国产免费久久精品国产传媒 | 综合人妻久久一区二区精品 | 熟妇激情内射com | 国产亚洲精品久久久闺蜜 | www国产精品内射老师 | 国产高清av在线播放 | 国产两女互慰高潮视频在线观看 | 亚洲精品一区二区三区在线 | 自拍偷自拍亚洲精品被多人伦好爽 | 国产一区二区三区影院 | 2019午夜福利不卡片在线 | 亚洲 日韩 欧美 成人 在线观看 | 国产女主播喷水视频在线观看 | 亚洲色大成网站www国产 | 一本久久a久久精品vr综合 | 暴力强奷在线播放无码 | 性史性农村dvd毛片 | 欧美成人家庭影院 | 国产黑色丝袜在线播放 | 成人片黄网站色大片免费观看 | 熟妇人妻无码xxx视频 | 四虎国产精品一区二区 | 午夜福利一区二区三区在线观看 | 无码国产色欲xxxxx视频 | 美女毛片一区二区三区四区 | 小鲜肉自慰网站xnxx | 小sao货水好多真紧h无码视频 | 国产免费无码一区二区视频 | 搡女人真爽免费视频大全 | 一本精品99久久精品77 | 欧美日韩一区二区免费视频 | 在线 国产 欧美 亚洲 天堂 | 国产色视频一区二区三区 | 女人高潮内射99精品 | 丰满人妻精品国产99aⅴ | 天堂亚洲2017在线观看 | 任你躁在线精品免费 | 亚洲国精产品一二二线 | 一本久久a久久精品亚洲 | 国产精品久久久久久亚洲毛片 | 精品国产一区二区三区四区在线看 | 激情内射日本一区二区三区 | 性欧美大战久久久久久久 | 精品国产福利一区二区 | 国产女主播喷水视频在线观看 | 小鲜肉自慰网站xnxx | 国产av一区二区三区最新精品 | 亚洲精品美女久久久久久久 | 国产成人人人97超碰超爽8 | 荫蒂被男人添的好舒服爽免费视频 | 小鲜肉自慰网站xnxx | 国产9 9在线 | 中文 | 暴力强奷在线播放无码 | 欧美老人巨大xxxx做受 | 人妻插b视频一区二区三区 | 性欧美大战久久久久久久 | 夜先锋av资源网站 | 精品国偷自产在线 | 国产内射爽爽大片视频社区在线 | 18黄暴禁片在线观看 | 强伦人妻一区二区三区视频18 | 国产免费久久精品国产传媒 | 国产av无码专区亚洲awww | 中国大陆精品视频xxxx | 中文字幕无码日韩专区 | 图片小说视频一区二区 | 女高中生第一次破苞av | 精品国产一区二区三区四区 | 成人精品天堂一区二区三区 | 波多野结衣aⅴ在线 | 国产又粗又硬又大爽黄老大爷视 | 亚洲精品成人av在线 | 中文字幕精品av一区二区五区 | 亚洲中文字幕无码中文字在线 | 亚洲精品鲁一鲁一区二区三区 | 正在播放东北夫妻内射 | a片免费视频在线观看 | 国产精品无码mv在线观看 | 久久久精品人妻久久影视 | 一本色道久久综合亚洲精品不卡 | 国产成人无码午夜视频在线观看 | 亚洲精品一区二区三区在线 | 熟妇人妻无乱码中文字幕 | 丝袜 中出 制服 人妻 美腿 | 无码任你躁久久久久久久 | 国产艳妇av在线观看果冻传媒 | 国产精品鲁鲁鲁 | 国产亚洲精品久久久久久国模美 | 在线成人www免费观看视频 | 久久午夜夜伦鲁鲁片无码免费 | 亚洲成av人综合在线观看 | 亚洲成色www久久网站 | 啦啦啦www在线观看免费视频 | av小次郎收藏 | 伊人久久大香线焦av综合影院 | 无码帝国www无码专区色综合 | 麻豆人妻少妇精品无码专区 | 欧美野外疯狂做受xxxx高潮 | 国产色在线 | 国产 | 国产亚av手机在线观看 | 伦伦影院午夜理论片 | а天堂中文在线官网 | 天堂а√在线地址中文在线 | 欧美国产亚洲日韩在线二区 | 高潮喷水的毛片 | 色情久久久av熟女人妻网站 | 18精品久久久无码午夜福利 | 亚洲一区二区三区偷拍女厕 | а√资源新版在线天堂 | 黑人玩弄人妻中文在线 | 国产无遮挡又黄又爽又色 | 99国产精品白浆在线观看免费 | 波多野结衣乳巨码无在线观看 | 亚洲熟妇色xxxxx欧美老妇 | 国产三级久久久精品麻豆三级 | 中文字幕av无码一区二区三区电影 | 国产亚洲人成在线播放 | 日本饥渴人妻欲求不满 | 国内精品人妻无码久久久影院 | 67194成是人免费无码 | 国产亚洲精品久久久久久久久动漫 | 国精产品一品二品国精品69xx | 国产精品永久免费视频 | 亚洲の无码国产の无码步美 | 俄罗斯老熟妇色xxxx | 国产精品久久久久无码av色戒 | 久久zyz资源站无码中文动漫 | 亚洲国产精品美女久久久久 | 国产乱人伦av在线无码 | 亚洲爆乳无码专区 | 亚洲成av人综合在线观看 | 妺妺窝人体色www婷婷 | 久久午夜无码鲁丝片 | 精品偷自拍另类在线观看 | 国产av久久久久精东av | 好屌草这里只有精品 | √天堂中文官网8在线 | 狂野欧美性猛xxxx乱大交 | 国产乱子伦视频在线播放 | 77777熟女视频在线观看 а天堂中文在线官网 | 日本一卡2卡3卡四卡精品网站 | 鲁鲁鲁爽爽爽在线视频观看 | 1000部夫妻午夜免费 | 精品国产福利一区二区 | 少妇厨房愉情理9仑片视频 | 无码成人精品区在线观看 | 精品国精品国产自在久国产87 | 成人一区二区免费视频 | 国内少妇偷人精品视频免费 | 内射欧美老妇wbb | 欧美喷潮久久久xxxxx | 性啪啪chinese东北女人 | 在线亚洲高清揄拍自拍一品区 | 中文字幕人妻无码一夲道 | 成熟妇人a片免费看网站 | 亚洲狠狠色丁香婷婷综合 | 婷婷六月久久综合丁香 | 成 人影片 免费观看 | 乱人伦人妻中文字幕无码久久网 | 国产福利视频一区二区 | av无码电影一区二区三区 | 麻豆国产97在线 | 欧洲 | 99久久精品国产一区二区蜜芽 | 国产成人精品三级麻豆 | 爱做久久久久久 | 蜜桃无码一区二区三区 | 久久天天躁狠狠躁夜夜免费观看 | 波多野42部无码喷潮在线 | 色欲人妻aaaaaaa无码 | 欧美熟妇另类久久久久久不卡 | 国产精品人人妻人人爽 | 动漫av一区二区在线观看 | 久久视频在线观看精品 | 免费观看又污又黄的网站 | 国产成人无码av一区二区 | 日日夜夜撸啊撸 | 久久综合九色综合欧美狠狠 | 色婷婷综合中文久久一本 | 久久午夜无码鲁丝片 | 亚洲一区av无码专区在线观看 | 99久久精品无码一区二区毛片 | 亚洲小说图区综合在线 | 日韩精品无码一本二本三本色 | 中文字幕精品av一区二区五区 | 日本一区二区三区免费播放 | 午夜福利试看120秒体验区 | 色综合久久久无码网中文 | 人妻中文无码久热丝袜 | 久久综合狠狠综合久久综合88 | 正在播放东北夫妻内射 | 国产精品久久久久久久影院 | 久久国产精品精品国产色婷婷 | 亚洲 欧美 激情 小说 另类 | 国内少妇偷人精品视频 | 精品成人av一区二区三区 | 西西人体www44rt大胆高清 | 午夜福利电影 | a国产一区二区免费入口 | 在线欧美精品一区二区三区 | 中文字幕无码视频专区 | 99er热精品视频 | 无码人妻精品一区二区三区不卡 | 国产黄在线观看免费观看不卡 | 精品人妻av区 | 亚洲国产成人av在线观看 | 3d动漫精品啪啪一区二区中 | 无码国内精品人妻少妇 | 人人妻人人澡人人爽人人精品浪潮 | 日韩欧美中文字幕公布 | 中文字幕 亚洲精品 第1页 | 国产av剧情md精品麻豆 | 国产亚洲精品久久久久久国模美 | 久久精品国产亚洲精品 | 国产舌乚八伦偷品w中 | 国产精品人人妻人人爽 | 欧美三级不卡在线观看 | 亚洲国精产品一二二线 | 一二三四在线观看免费视频 | 午夜成人1000部免费视频 | 成人性做爰aaa片免费看 | 亚洲精品国产品国语在线观看 | 3d动漫精品啪啪一区二区中 | 国产一精品一av一免费 | 377p欧洲日本亚洲大胆 | 亚洲无人区午夜福利码高清完整版 | 美女黄网站人色视频免费国产 | 久久人人爽人人爽人人片av高清 | 少妇性l交大片欧洲热妇乱xxx | 亚洲国产精品毛片av不卡在线 | 亚洲区欧美区综合区自拍区 | 宝宝好涨水快流出来免费视频 | 久9re热视频这里只有精品 | 亚洲综合无码久久精品综合 | 波多野42部无码喷潮在线 | 亚洲s码欧洲m码国产av | 国产精品久久久 | 免费无码一区二区三区蜜桃大 | 性欧美videos高清精品 | 一区二区传媒有限公司 | 欧美丰满熟妇xxxx性ppx人交 | 夜夜高潮次次欢爽av女 | 久久午夜无码鲁丝片秋霞 | 免费看男女做好爽好硬视频 | 国产亚洲美女精品久久久2020 | 亚洲aⅴ无码成人网站国产app | 青草视频在线播放 | 国产绳艺sm调教室论坛 | 小sao货水好多真紧h无码视频 | 强开小婷嫩苞又嫩又紧视频 | 一本大道久久东京热无码av | 无码播放一区二区三区 | 国内精品九九久久久精品 | 在线a亚洲视频播放在线观看 | 中文字幕色婷婷在线视频 | 国产在热线精品视频 | 无码福利日韩神码福利片 | 激情爆乳一区二区三区 | 天天av天天av天天透 | 人人妻人人藻人人爽欧美一区 | 国产精品永久免费视频 | 精品人妻中文字幕有码在线 | 亚洲成色www久久网站 | 国产无av码在线观看 | 无码av免费一区二区三区试看 | 欧美日韩人成综合在线播放 | 亚洲小说图区综合在线 | 狠狠综合久久久久综合网 | 久久视频在线观看精品 | 99精品久久毛片a片 | 亚洲日韩乱码中文无码蜜桃臀网站 | aⅴ在线视频男人的天堂 | 精品久久久久久亚洲精品 | 亚洲国产精品无码一区二区三区 | 99精品久久毛片a片 | 蜜桃视频韩日免费播放 | 欧美人与善在线com | 麻豆人妻少妇精品无码专区 | 久久久久久久女国产乱让韩 | 国产sm调教视频在线观看 | 国产无套粉嫩白浆在线 | 久久人人爽人人爽人人片ⅴ | 2020久久香蕉国产线看观看 | 丰满护士巨好爽好大乳 | 国产人成高清在线视频99最全资源 | 国产精品爱久久久久久久 | 久久精品成人欧美大片 | 国产成人综合色在线观看网站 | 纯爱无遮挡h肉动漫在线播放 | 久久婷婷五月综合色国产香蕉 | 永久免费精品精品永久-夜色 | 精品欧美一区二区三区久久久 | 精品一二三区久久aaa片 | 精品 日韩 国产 欧美 视频 | 纯爱无遮挡h肉动漫在线播放 | 国产日产欧产精品精品app | 女人被男人躁得好爽免费视频 | 性做久久久久久久久 | 日本护士xxxxhd少妇 | 国产女主播喷水视频在线观看 | 小泽玛莉亚一区二区视频在线 | 欧美日韩在线亚洲综合国产人 | 88国产精品欧美一区二区三区 | 欧美乱妇无乱码大黄a片 | 国产一区二区不卡老阿姨 | 麻豆果冻传媒2021精品传媒一区下载 | 国产无遮挡吃胸膜奶免费看 | 亚洲人成影院在线观看 | 亚洲国产精品无码久久久久高潮 | 人人爽人人爽人人片av亚洲 | 国产午夜手机精彩视频 | 国产精品鲁鲁鲁 | 67194成是人免费无码 | 中文毛片无遮挡高清免费 | 精品国产aⅴ无码一区二区 | 丰满人妻一区二区三区免费视频 | 天海翼激烈高潮到腰振不止 | 色情久久久av熟女人妻网站 | 国产色精品久久人妻 | 国产精品久久久一区二区三区 | 亚洲国产精品毛片av不卡在线 | 日韩av激情在线观看 | 无码人妻丰满熟妇区毛片18 | 亚洲七七久久桃花影院 | 亚洲va中文字幕无码久久不卡 | 久久久久亚洲精品男人的天堂 | 亚洲综合在线一区二区三区 | 乱人伦人妻中文字幕无码 | 欧洲欧美人成视频在线 | 精品无码国产自产拍在线观看蜜 | 久久综合久久自在自线精品自 | 熟妇女人妻丰满少妇中文字幕 | 亚洲s色大片在线观看 | 十八禁真人啪啪免费网站 | 牲欲强的熟妇农村老妇女视频 | 奇米影视7777久久精品人人爽 | 亚洲国产精品无码一区二区三区 | 国产成人无码区免费内射一片色欲 | 精品久久久久久人妻无码中文字幕 | 亚洲人亚洲人成电影网站色 | 亚洲成a人一区二区三区 | 初尝人妻少妇中文字幕 | 极品尤物被啪到呻吟喷水 | 男女猛烈xx00免费视频试看 | 波多野结衣高清一区二区三区 | 永久免费观看国产裸体美女 | 久久99热只有频精品8 | 亚洲色www成人永久网址 | 亚洲一区二区三区四区 | 一本加勒比波多野结衣 | 精品久久久无码中文字幕 | 亚洲国产精品无码久久久久高潮 | 亚洲午夜久久久影院 | 97精品人妻一区二区三区香蕉 | 久久综合给久久狠狠97色 | 亚洲成色www久久网站 | 午夜丰满少妇性开放视频 | 亚洲精品成a人在线观看 | 人妻少妇精品无码专区动漫 | 少妇被黑人到高潮喷出白浆 | 久久久久免费精品国产 | 少妇性俱乐部纵欲狂欢电影 | 少妇人妻av毛片在线看 | 国产精品福利视频导航 | 久久精品一区二区三区四区 | 人人妻人人藻人人爽欧美一区 | 色综合久久久无码网中文 | 樱花草在线播放免费中文 | 特大黑人娇小亚洲女 | 无码中文字幕色专区 | 久久久久亚洲精品男人的天堂 | 亚洲日韩av一区二区三区中文 | 内射后入在线观看一区 | 三上悠亚人妻中文字幕在线 | 国产精品第一区揄拍无码 | 青春草在线视频免费观看 | 丰腴饱满的极品熟妇 | 成人女人看片免费视频放人 | 色综合久久网 | 免费国产黄网站在线观看 | 免费人成在线观看网站 | 国产av无码专区亚洲a∨毛片 | 国产在热线精品视频 | 99国产精品白浆在线观看免费 | 人妻夜夜爽天天爽三区 | 女人被男人躁得好爽免费视频 | 国产超级va在线观看视频 | 成人无码视频免费播放 | 成人无码精品1区2区3区免费看 | 巨爆乳无码视频在线观看 | 蜜桃臀无码内射一区二区三区 | 精品久久久中文字幕人妻 | 国产在线精品一区二区高清不卡 | 综合激情五月综合激情五月激情1 | 一本一道久久综合久久 | v一区无码内射国产 | 无码纯肉视频在线观看 | 精品日本一区二区三区在线观看 | 熟妇人妻无乱码中文字幕 | 呦交小u女精品视频 | 久久国产精品萌白酱免费 | 在线看片无码永久免费视频 | 欧美高清在线精品一区 | 日本高清一区免费中文视频 | 精品一区二区三区无码免费视频 | 国产精品久久久久9999小说 | 人妻少妇精品无码专区二区 | 人妻与老人中文字幕 | 亚洲国精产品一二二线 | 无码精品人妻一区二区三区av | 大地资源中文第3页 | 日韩视频 中文字幕 视频一区 | 色窝窝无码一区二区三区色欲 | 国产精品二区一区二区aⅴ污介绍 | 国产成人一区二区三区别 | 午夜精品一区二区三区的区别 | 一区二区传媒有限公司 | 99re在线播放 | 欧美性猛交内射兽交老熟妇 | 日本xxxx色视频在线观看免费 | 内射老妇bbwx0c0ck | 久久久成人毛片无码 | 国产精品亚洲综合色区韩国 | 国产69精品久久久久app下载 | 国产乱人伦app精品久久 国产在线无码精品电影网 国产国产精品人在线视 | 国产香蕉97碰碰久久人人 | 一二三四在线观看免费视频 | 波多野结衣乳巨码无在线观看 | 国产两女互慰高潮视频在线观看 | 精品久久8x国产免费观看 | 久久99久久99精品中文字幕 | 亚洲呦女专区 | 蜜臀aⅴ国产精品久久久国产老师 | 精品无码一区二区三区的天堂 | 久热国产vs视频在线观看 | 国产精品亚洲一区二区三区喷水 | 一本无码人妻在中文字幕免费 | 好爽又高潮了毛片免费下载 | 性啪啪chinese东北女人 | 久久久久亚洲精品男人的天堂 | √天堂中文官网8在线 | 日日橹狠狠爱欧美视频 | 亚洲国产精品毛片av不卡在线 | 99久久人妻精品免费二区 | 亚洲高清偷拍一区二区三区 | 天堂在线观看www | 国产精品多人p群无码 | 美女极度色诱视频国产 | 欧美日本精品一区二区三区 | 精品无人国产偷自产在线 | 亚洲色无码一区二区三区 | 亚洲gv猛男gv无码男同 | 久久久久av无码免费网 | 老熟女重囗味hdxx69 | 日本又色又爽又黄的a片18禁 | 精品一区二区三区波多野结衣 | 四十如虎的丰满熟妇啪啪 | 97人妻精品一区二区三区 | 又大又黄又粗又爽的免费视频 | 亚洲精品国偷拍自产在线麻豆 | 亚洲日本va中文字幕 | www国产亚洲精品久久网站 | 亚洲国产精品一区二区美利坚 | 久久国内精品自在自线 | 毛片内射-百度 | yw尤物av无码国产在线观看 | 六十路熟妇乱子伦 | 欧美激情一区二区三区成人 | 国产成人久久精品流白浆 | 综合人妻久久一区二区精品 | 亚洲欧美中文字幕5发布 | 无码纯肉视频在线观看 | 激情爆乳一区二区三区 | 成人亚洲精品久久久久软件 | 久久精品国产一区二区三区肥胖 | 亚洲熟女一区二区三区 | 激情国产av做激情国产爱 | 欧美亚洲日韩国产人成在线播放 | 天干天干啦夜天干天2017 | 成人无码精品一区二区三区 | 亚洲国产精品久久人人爱 | 无码吃奶揉捏奶头高潮视频 | 久久精品国产大片免费观看 | 国产色视频一区二区三区 | 国产成人无码av在线影院 | 少妇激情av一区二区 | 国产9 9在线 | 中文 | 日本精品人妻无码77777 天堂一区人妻无码 | 免费无码一区二区三区蜜桃大 | 中文字幕中文有码在线 | 国产精品久久久久影院嫩草 | 中文字幕乱码人妻无码久久 | 久久久久av无码免费网 | 久久久精品456亚洲影院 | 亚洲爆乳无码专区 | 国产猛烈高潮尖叫视频免费 | 日日摸日日碰夜夜爽av | 国内揄拍国内精品人妻 | 国产无遮挡吃胸膜奶免费看 | 国产女主播喷水视频在线观看 | 国产精品第一国产精品 | 噜噜噜亚洲色成人网站 | 欧美三级不卡在线观看 | 伊在人天堂亚洲香蕉精品区 | 久9re热视频这里只有精品 | 亚洲精品国产精品乱码不卡 | 一本久久a久久精品vr综合 | 人妻无码久久精品人妻 | 色综合久久久久综合一本到桃花网 | 国产香蕉尹人综合在线观看 | 国产精品久免费的黄网站 | 日韩精品无码免费一区二区三区 | 丰满人妻翻云覆雨呻吟视频 | 亚洲欧洲日本无在线码 | 国产成人精品三级麻豆 | 玩弄中年熟妇正在播放 | 欧美日韩色另类综合 | 亚洲成av人片在线观看无码不卡 | 色综合久久久久综合一本到桃花网 | 小sao货水好多真紧h无码视频 | 亚洲欧洲日本无在线码 | 九一九色国产 | 欧美性生交活xxxxxdddd | 国内综合精品午夜久久资源 | 亚洲a无码综合a国产av中文 | 99久久亚洲精品无码毛片 | 国产精华av午夜在线观看 | 少妇被粗大的猛进出69影院 | 国产三级久久久精品麻豆三级 | 国产乱人无码伦av在线a | 亚洲国产精华液网站w | 人人妻人人澡人人爽人人精品浪潮 | 亚洲中文字幕成人无码 | 国产激情无码一区二区app | 波多野结衣乳巨码无在线观看 | 亚洲精品成a人在线观看 | 大肉大捧一进一出视频出来呀 | 国产免费久久久久久无码 | 大肉大捧一进一出视频出来呀 | 东京热男人av天堂 | 日韩欧美成人免费观看 | 东北女人啪啪对白 | 精品一二三区久久aaa片 | 免费人成网站视频在线观看 | 丰满少妇人妻久久久久久 | 欧美丰满少妇xxxx性 | 男人扒开女人内裤强吻桶进去 | 丰满人妻被黑人猛烈进入 | 国产无套粉嫩白浆在线 | 国产免费无码一区二区视频 | 精品国产麻豆免费人成网站 | 亚洲精品欧美二区三区中文字幕 | 国产欧美精品一区二区三区 | 国产精品怡红院永久免费 | 奇米影视7777久久精品人人爽 | 伊人久久婷婷五月综合97色 | 国产午夜精品一区二区三区嫩草 | 无码成人精品区在线观看 | 国产精品对白交换视频 | 免费观看又污又黄的网站 | 亚洲狠狠色丁香婷婷综合 | 内射后入在线观看一区 | 亚洲欧洲日本综合aⅴ在线 | 国产小呦泬泬99精品 | 欧美精品无码一区二区三区 | 亚洲精品国产a久久久久久 | 无码av岛国片在线播放 | 国产真实乱对白精彩久久 | 天下第一社区视频www日本 | 丰满少妇女裸体bbw | 久久99热只有频精品8 | 亚洲爆乳精品无码一区二区三区 | 黑人粗大猛烈进出高潮视频 | 激情亚洲一区国产精品 | 国产精品久久久一区二区三区 | 国产精品高潮呻吟av久久4虎 | 亚洲精品国产精品乱码视色 | 亚洲欧美国产精品专区久久 | 国产精品亚洲а∨无码播放麻豆 | 久久午夜无码鲁丝片午夜精品 | 成人av无码一区二区三区 | a在线亚洲男人的天堂 | 蜜桃无码一区二区三区 | 亚洲区小说区激情区图片区 | 狠狠综合久久久久综合网 | 全黄性性激高免费视频 | 男女下面进入的视频免费午夜 | 国产国语老龄妇女a片 | 欧美国产亚洲日韩在线二区 | 夜夜高潮次次欢爽av女 | 亚洲一区二区三区播放 | 97夜夜澡人人双人人人喊 | 国产偷国产偷精品高清尤物 | 纯爱无遮挡h肉动漫在线播放 | 国产人妻精品一区二区三区 | 亚洲自偷精品视频自拍 | 午夜精品一区二区三区的区别 | 亚洲爆乳无码专区 | av在线亚洲欧洲日产一区二区 | 亚洲精品久久久久avwww潮水 | 国产在线精品一区二区三区直播 | 亚洲人成网站在线播放942 | 国产熟妇高潮叫床视频播放 | 午夜肉伦伦影院 | 久久精品女人天堂av免费观看 | 亚洲国产欧美在线成人 | 成年美女黄网站色大免费视频 | 亚洲午夜无码久久 | 色狠狠av一区二区三区 | 性史性农村dvd毛片 | 亚洲小说春色综合另类 | 国产精品亚洲专区无码不卡 | 日韩精品乱码av一区二区 | 久久精品女人天堂av免费观看 | 中文字幕无码免费久久9一区9 | 中文字幕无码人妻少妇免费 | 少妇性l交大片欧洲热妇乱xxx | 久久久久人妻一区精品色欧美 | 国产精品资源一区二区 | 1000部啪啪未满十八勿入下载 | 中文字幕av日韩精品一区二区 | 国产日产欧产精品精品app | 亚洲精品成人av在线 | 性欧美videos高清精品 | v一区无码内射国产 | 天堂无码人妻精品一区二区三区 | 国产精品久久久午夜夜伦鲁鲁 | 国产特级毛片aaaaaaa高清 | 美女黄网站人色视频免费国产 | 久久久精品456亚洲影院 | 国产av无码专区亚洲a∨毛片 | 人人爽人人爽人人片av亚洲 | 人人超人人超碰超国产 | 国产成人无码av一区二区 | 日韩欧美中文字幕在线三区 | 一个人看的www免费视频在线观看 | 99麻豆久久久国产精品免费 | 精品国产一区av天美传媒 | yw尤物av无码国产在线观看 | 国产熟女一区二区三区四区五区 | 99在线 | 亚洲 | 精品少妇爆乳无码av无码专区 | 亚洲精品一区二区三区大桥未久 | 亚洲一区二区三区含羞草 | 夜精品a片一区二区三区无码白浆 | 亚洲男人av香蕉爽爽爽爽 | 久久国产36精品色熟妇 | 亚洲精品国产精品乱码不卡 | 久久熟妇人妻午夜寂寞影院 | 偷窥日本少妇撒尿chinese | 亚洲经典千人经典日产 | 国产高清av在线播放 | 色噜噜亚洲男人的天堂 | 午夜男女很黄的视频 | 最近的中文字幕在线看视频 | 国产情侣作爱视频免费观看 | 未满成年国产在线观看 | 国产综合在线观看 | 伊人久久婷婷五月综合97色 | 久久久精品成人免费观看 | 国产深夜福利视频在线 | 色综合久久久久综合一本到桃花网 | 国产精华av午夜在线观看 | 无套内谢老熟女 | 免费男性肉肉影院 | 爆乳一区二区三区无码 | 国产av人人夜夜澡人人爽麻豆 | 中文字幕乱码人妻无码久久 | 亚洲码国产精品高潮在线 | aa片在线观看视频在线播放 | 97精品国产97久久久久久免费 | 日韩av激情在线观看 | 国产一区二区三区四区五区加勒比 | 久久精品国产一区二区三区肥胖 | 国色天香社区在线视频 | 久久99精品国产.久久久久 | 欧美乱妇无乱码大黄a片 | 国产精品对白交换视频 | 国产精品内射视频免费 | 久久综合狠狠综合久久综合88 | 久久熟妇人妻午夜寂寞影院 | 天天躁日日躁狠狠躁免费麻豆 | 中文字幕av日韩精品一区二区 | 狠狠色噜噜狠狠狠狠7777米奇 | 人人超人人超碰超国产 | v一区无码内射国产 | 亚洲一区av无码专区在线观看 | 麻豆国产丝袜白领秘书在线观看 | 色偷偷人人澡人人爽人人模 | 特级做a爰片毛片免费69 | 亚洲色在线无码国产精品不卡 | 少妇高潮一区二区三区99 | 国产麻豆精品一区二区三区v视界 | 日韩人妻无码一区二区三区久久99 | 午夜福利不卡在线视频 | 少妇久久久久久人妻无码 | 亚洲精品综合一区二区三区在线 | 国产免费久久久久久无码 | 好爽又高潮了毛片免费下载 | 久久99精品久久久久婷婷 | 老子影院午夜伦不卡 | 欧美成人免费全部网站 | 精品久久8x国产免费观看 | 色综合久久88色综合天天 | 中文字幕乱妇无码av在线 | 老太婆性杂交欧美肥老太 | 国产成人无码av一区二区 | 久久久久亚洲精品男人的天堂 | 日本大香伊一区二区三区 | 国产精品第一国产精品 | 成人无码精品1区2区3区免费看 | 亚洲爆乳无码专区 | 国产真实乱对白精彩久久 | 国产精品鲁鲁鲁 | 国产成人亚洲综合无码 | 日韩无码专区 | 天天躁日日躁狠狠躁免费麻豆 | 国产偷抇久久精品a片69 | 亚洲国产精品一区二区第一页 | 国产精品久久久久久亚洲毛片 | 久久zyz资源站无码中文动漫 | 性开放的女人aaa片 | 撕开奶罩揉吮奶头视频 | 99精品视频在线观看免费 | 久久无码中文字幕免费影院蜜桃 | 亚洲精品中文字幕乱码 | 亚洲区欧美区综合区自拍区 | 丰满人妻翻云覆雨呻吟视频 | 自拍偷自拍亚洲精品10p | 麻豆精品国产精华精华液好用吗 | 欧美日本免费一区二区三区 | 鲁鲁鲁爽爽爽在线视频观看 | 免费无码av一区二区 | 精品人妻中文字幕有码在线 | 午夜福利不卡在线视频 | 国产精品久久久久久无码 | 久久久久av无码免费网 | 少妇久久久久久人妻无码 | 国产网红无码精品视频 | 欧美三级不卡在线观看 | 免费无码一区二区三区蜜桃大 | www国产亚洲精品久久网站 | 天天爽夜夜爽夜夜爽 | 欧美亚洲日韩国产人成在线播放 | 国产精品国产自线拍免费软件 | 久久精品视频在线看15 | 亚洲国产综合无码一区 | 精品无码国产自产拍在线观看蜜 | 中文无码精品a∨在线观看不卡 | 青春草在线视频免费观看 | 高中生自慰www网站 | 中文字幕色婷婷在线视频 | 免费观看的无遮挡av | 福利一区二区三区视频在线观看 | 国产在线aaa片一区二区99 | 久久亚洲国产成人精品性色 | 欧美自拍另类欧美综合图片区 | 老子影院午夜伦不卡 | 国产色在线 | 国产 | 鲁鲁鲁爽爽爽在线视频观看 | 一区二区三区高清视频一 | 荫蒂添的好舒服视频囗交 | 欧美丰满老熟妇xxxxx性 | 国产精品资源一区二区 | 在线观看欧美一区二区三区 | 久久久国产一区二区三区 | 东京热一精品无码av | 在线亚洲高清揄拍自拍一品区 | 黑人巨大精品欧美黑寡妇 | 伊人久久婷婷五月综合97色 | 沈阳熟女露脸对白视频 | 成熟女人特级毛片www免费 | 国产精品久久久久久久影院 | 六月丁香婷婷色狠狠久久 | 日韩亚洲欧美精品综合 | 永久黄网站色视频免费直播 | 99riav国产精品视频 | 久久伊人色av天堂九九小黄鸭 | 久久婷婷五月综合色国产香蕉 | 高潮毛片无遮挡高清免费 | 亚洲精品久久久久久一区二区 | 又大又硬又爽免费视频 | 亚洲国产高清在线观看视频 | 色妞www精品免费视频 | 国产精品无码一区二区三区不卡 | 欧美人与牲动交xxxx | 亚洲成a人一区二区三区 | 自拍偷自拍亚洲精品被多人伦好爽 | 97精品人妻一区二区三区香蕉 | 又紧又大又爽精品一区二区 | 亚洲一区二区三区无码久久 | 98国产精品综合一区二区三区 | 内射巨臀欧美在线视频 | 久久久久免费看成人影片 | 久久久久久国产精品无码下载 | 波多野结衣av一区二区全免费观看 | 狠狠色色综合网站 | 欧美国产日韩久久mv | 久激情内射婷内射蜜桃人妖 | 无码人妻精品一区二区三区不卡 | 18无码粉嫩小泬无套在线观看 | 少妇被粗大的猛进出69影院 | 久久久久人妻一区精品色欧美 | 亚洲の无码国产の无码步美 | 麻豆国产人妻欲求不满谁演的 | 亚洲欧美中文字幕5发布 | 国产精品永久免费视频 | 亚洲精品国偷拍自产在线观看蜜桃 | 澳门永久av免费网站 | 丰满少妇熟乱xxxxx视频 | 国产成人综合色在线观看网站 | 亚洲一区av无码专区在线观看 | 免费无码的av片在线观看 | 无码人妻少妇伦在线电影 | 亚洲精品一区二区三区大桥未久 | 亚洲成a人片在线观看无码 | 狠狠色噜噜狠狠狠狠7777米奇 | 一二三四社区在线中文视频 | 免费无码的av片在线观看 | 欧美真人作爱免费视频 | a在线亚洲男人的天堂 | 国内揄拍国内精品人妻 | 亚洲人成影院在线无码按摩店 | 精品国产青草久久久久福利 | 小鲜肉自慰网站xnxx | 特黄特色大片免费播放器图片 | 性欧美熟妇videofreesex | 亚洲一区二区三区四区 | 中文字幕 亚洲精品 第1页 | 亚洲成av人片天堂网无码】 | 久久综合久久自在自线精品自 | 日韩精品无码一本二本三本色 | 亚洲日本在线电影 | 精品久久久久久亚洲精品 | 强开小婷嫩苞又嫩又紧视频 | 麻豆成人精品国产免费 | 男女爱爱好爽视频免费看 | 欧美性猛交xxxx富婆 | 天天摸天天碰天天添 | 欧美阿v高清资源不卡在线播放 | 东京无码熟妇人妻av在线网址 | 国产欧美精品一区二区三区 | 日日干夜夜干 | 中文字幕无码乱人伦 | 日韩av无码一区二区三区不卡 | 久久无码中文字幕免费影院蜜桃 | 欧美 丝袜 自拍 制服 另类 | 亚洲中文字幕在线无码一区二区 | 久久精品人人做人人综合试看 | 日本乱偷人妻中文字幕 | 又大又硬又爽免费视频 | 少妇厨房愉情理9仑片视频 | 久久综合网欧美色妞网 | 国产精品久久久 | 国产日产欧产精品精品app | 无码任你躁久久久久久久 | 国语自产偷拍精品视频偷 | 熟女少妇人妻中文字幕 | 欧美丰满熟妇xxxx | 国产熟妇高潮叫床视频播放 | 麻豆人妻少妇精品无码专区 | 无码国产色欲xxxxx视频 | 亚洲色欲色欲欲www在线 | 国产精品爱久久久久久久 | 男人的天堂av网站 | 欧美老妇与禽交 | 狠狠噜狠狠狠狠丁香五月 | 男人和女人高潮免费网站 | 曰韩无码二三区中文字幕 | 国产成人一区二区三区别 | 亚洲男人av香蕉爽爽爽爽 | 乱人伦人妻中文字幕无码 | 国产精品香蕉在线观看 | 最新国产乱人伦偷精品免费网站 | 99久久久国产精品无码免费 | 丰满少妇女裸体bbw | 特黄特色大片免费播放器图片 | 综合激情五月综合激情五月激情1 | 一本久久伊人热热精品中文字幕 | 日韩人妻无码一区二区三区久久99 | 99久久久国产精品无码免费 | 国产亚洲精品久久久久久久 | 成人精品视频一区二区三区尤物 | 久久精品视频在线看15 | 老熟女乱子伦 | 国产美女极度色诱视频www | 精品少妇爆乳无码av无码专区 | 97久久精品无码一区二区 | 欧美国产日韩久久mv | 丰满少妇人妻久久久久久 | 人妻互换免费中文字幕 | 国产精品亚洲一区二区三区喷水 | 亚欧洲精品在线视频免费观看 | 扒开双腿吃奶呻吟做受视频 | 国产精品免费大片 | 成人试看120秒体验区 | 久久久久99精品成人片 | 久久无码人妻影院 | 日本熟妇人妻xxxxx人hd | 亚洲色在线无码国产精品不卡 | 我要看www免费看插插视频 | 精品国产一区二区三区av 性色 | 亚洲狠狠婷婷综合久久 | 99精品视频在线观看免费 | 日韩精品a片一区二区三区妖精 | 无码午夜成人1000部免费视频 | 国产精品-区区久久久狼 | 成人影院yy111111在线观看 | 99国产欧美久久久精品 | 国产成人无码a区在线观看视频app | 澳门永久av免费网站 | 亚洲欧洲中文日韩av乱码 | 久久久久人妻一区精品色欧美 | 一本无码人妻在中文字幕免费 | 一本大道伊人av久久综合 | 高中生自慰www网站 | 欧美放荡的少妇 | 又色又爽又黄的美女裸体网站 | 国产精品久久久久无码av色戒 | 特大黑人娇小亚洲女 | 国产午夜精品一区二区三区嫩草 | 国产熟妇高潮叫床视频播放 | 熟妇女人妻丰满少妇中文字幕 | 欧美喷潮久久久xxxxx | 久久99精品国产.久久久久 | 亚洲中文字幕无码中字 | 55夜色66夜色国产精品视频 | 粉嫩少妇内射浓精videos | 国产亚洲人成a在线v网站 | 国产综合在线观看 | 久久综合色之久久综合 | 亚洲欧美日韩国产精品一区二区 | 国产精品-区区久久久狼 | 又大又硬又黄的免费视频 | 性生交大片免费看女人按摩摩 | 好男人社区资源 | 国产精品无码一区二区三区不卡 | 久久久亚洲欧洲日产国码αv | 亚洲色偷偷男人的天堂 | 欧美黑人巨大xxxxx | 欧美人与善在线com | 日韩少妇内射免费播放 |