• 林丹红   2014/7/14 13:21:00
  • 开源搜索引擎框架Lucene.net学习之-Directory
  • 关键字: Lucene.net Directory
  • 在使用Lucene.Net中,第一个接触的类一般是Directory。它是Lucene存储的一个抽象,由此派生了两个类:FSDirectory和RAMDirectory,用于控制索引文件的存储位置。使用FSDirectory类,就是存储到硬盘;使用RAMDirectory类,则是存储到内存。
     

    图1-1 存储抽象实现UML图
     
    如图1-1,显示了这种关系。而看Lucene代码会发现,RAMDirectory和FSDirectory还分别有一个内嵌类。这个内嵌类实际上是通过工具从Java版本转移过来,工具自动产生的。那就先看看Java代码的结构,然后再来看转移过来生成的内嵌类是干什么用的。
     
    Directory类一共有11个方法,看看类的注释就知道,翻译过来也是没有做非常细致的调整。比如,注释上有这句话“Java's i/o APIs not used directly, but rather all i/o is through this API. ”。还保留着Java的解释了,虽然没有人会认为在dotNet平台会采用Java的API,但是,这句话确实够昏的。Directory的注释原文:
    A Directory is a flat list of files.  Files may be written once, when they are created.  Once a file is created it may only be opened for read, or
    deleted.  Random access is permitted both when reading and writing. Java's i/o APIs not used directly, but rather all i/o is through this API.  This permits things such as:

    implementation of RAM-based indices;
    implementation indices stored in a database, via JDBC;
    implementation of an index as a single file;

     

    意思就是:一个Directory对象是一份文件的清单。文件可能只在被创建的时候写一次。一旦文件被创建,它将只被读取或者删除。在读取的时候进行写入操作是允许的。Java的I/O库没有被直接使用,所以的I/O操作都通过这个API。这些存储可以允许:

    1.实现基于内存的索引;

    2.实现索引存入数据库,通过JDBC;

    3.实现一个索引是一个文件。


    而Directory的11个方法分别是:

    1、list    把一个Directory对象下的文件,按字符串数组的方式返回;

    2、fileExists  给定一个文件名,如果存在,就返回true;

    3、fileModified   返回给定文件名被修改的时间;

    4、touchFile     设置给定文件名文件的更新时间为现在;

    5、deleteFile     删除当前directory对象下一个给定文件名的文件,该文件必须存在;

    6、renameFile   重命名当前directory一个文件的文件名,如果新的名字在directory里已经存在,将会更换。这个要更换原子;

    7、fileLength    返回文件的长度;

    8、createFile    创建一个空文件,并且返回该文件的写入流;

    9、openFile     返回一个存在文件的读取流;

    10、makeLock    锁定该directory对象;

    11、close          关闭该对象。

     

    而在Directory类中,使用的都是抽象方法,把这个类换成接口也可以。

    然后再来看看RAMDirectory类。RAMDirectory是Directory的内存操作实现。RAMDirectory类有5个重载构造函数。

    RAMDirectory() 构造函数无操作

    RAMDirectory(Directory dir)   允许把硬盘上的索引载入内存,这个操作只适用于可以被载入内存的索引。(注:文件结构不对或者索引大小超出内存肯定就不行了。)这个构造函数只调用了RAMDirectory(Directory dir, boolean closeDir)构造函数,并未做其他动作。

    再来看看RAMDirectory(Directory dir, boolean closeDir)构造函数。除了默认构造函数,其他3个构造函数都是调用的这个构造函数做处理的。 

     代码 1-2:

     string[] files = dir.List();
                
    for (int i = 0; i < files.Length; i++)
                {
                    
    // make place on ram disk
                    OutputStream os = CreateFile(System.IO.Path.GetFileName(files[i]));
                    
    // read current file
                    InputStream is_Renamed = dir.OpenFile(files[i]);
                    
    // and copy to ram disk
                    int len = (int) is_Renamed.Length();
                    
    byte[] buf = new byte[len];
                    is_Renamed.ReadBytes(buf, 
    0, len);
                    os.WriteBytes(buf, len);
                    
    // graceful cleanup
                    is_Renamed.Close();
                    os.Close();
                }
                
    if (closeDir)
                    dir.Close();
     

    其他两个构造函数用到了FSDirectory类把文件构造成Directory对象。看看代码就行了:

     代码 1-3:

     public RAMDirectory(System.IO.FileInfo dir) : this(FSDirectory.GetDirectory(dir, false), true)
            {
            }
            
    public RAMDirectory(System.String dir) : this(FSDirectory.GetDirectory(dir, false), true)
            {
            }

     
    这两个构造函数第二个参数都是true,和
     
    代码 1-4:
     public RAMDirectory(Directory dir) : this(dir, false)
            {
            }
    这个不一样,那是因为,这个参数是控制是否关闭传入的对象构建或者直接创建的Directory对象。对于传入的,也就是代码1-4,因为这个传入的对象是个引用类型,这个如果被关闭,将影响到传递来的对象状态。而代码1-3是有它自身创建的Directory,关闭它并不会影响到RAMDirectory的外部。代码1-2的功能就是实现把硬盘上的索引按字节转存到内存中。
     
    首先,会创建一个文件,调用的是RAMDirectory自身的CreateFile方法:
     
    代码1-5
       public override OutputStream CreateFile(System.String name)
            {
                RAMFile file 
    = new RAMFile();
                files[name] 
    = file;
                
    return new RAMOutputStream(file);
            }
     
    这个方法调用了两个还没讲的类。实现的功能就是创建一个内存的文件映像。
     
    在RAMDirectory定义了一个Hashtable,这个哈希表会在CreateFile被调用时,往里面填充创建的文件。所以,从硬盘往内存拷贝文件的过程中,这个哈希表就记录下了内存中所有被创建的文件。在List方法,就可以通过枚举的方式来获取内存中文件的数量。
     
    代码 1-6
     public override System.String[] List()
            {
                System.String[] result 
    = new System.String[files.Count];
                
    int i = 0;
                System.Collections.IEnumerator names 
    = files.Keys.GetEnumerator();
                
    while (names.MoveNext())
                {
                    result[i
    ++= ((System.String) names.Current);
                }
                
    return result;
            }
     
    FileExists和FileModified 都比较简单,对照下CreateFile的代码很容易读懂:
    代码 1-7

            /// <summary>Returns true iff the named file exists in this directory. </summary>
            public override bool FileExists(System.String name)
            {
                RAMFile file 
    = (RAMFile) files[name];
                
    return file != null;
            }
            
            
    /// <summary>Returns the time the named file was last modified. </summary>
            public override long FileModified(System.String name)
            {
                RAMFile file 
    = (RAMFile) files[name];
                
    return file.lastModified;
            }
     
    TouchFile的代码比较长,需要看一下
     
    代码1-8
     

    /// <summary>Set the modified time of an existing file to now. </summary>
            public override void  TouchFile(System.String name)
            {
                
    //     final boolean MONITOR = false;
                
                RAMFile file 
    = (RAMFile) files[name];
                
    long ts2, ts1 = (System.DateTime.Now.Ticks - 621355968000000000/ 10000;
                
    do 
                {
                    
    try
                    {
                        System.Threading.Thread.Sleep(
    new System.TimeSpan((System.Int64) 10000 * 0 + 100 * 1));
                    }
                    
    catch (System.Threading.ThreadInterruptedException)
                    {
                    }
                    ts2 
    = (System.DateTime.Now.Ticks - 621355968000000000/ 10000;
                    
    //       if (MONITOR) {
                    
    //         count++;
                    
    //       }
                }
                
    while (ts1 == ts2);
                
                file.lastModified 
    = ts2;
                
                
    //     if (MONITOR)
                
    //         System.out.println("SLEEP COUNT: " + count);
            }通过文件名,可以从哈希表中还原出一个RAMFile对象,但是下面的代码比较难懂,为什么要减去个那数值呢?看看Java版的代码:

    代码1-9

     /** Set the modified time of an existing file to now. */
        
    public void touchFile(String name) throws IOException {
            
    // final boolean MONITOR = false;

            RAMFile file 
    = (RAMFile) files.get(name);
            
    long ts2, ts1 = System.currentTimeMillis();
            
    do {
                
    try {
                    Thread.sleep(
    01);
                } 
    catch (InterruptedException e) {
                }
                ts2 
    = System.currentTimeMillis();
                
    // if (MONITOR) {
                
    // count++;
                
    // }
            } while (ts1 == ts2);

            file.lastModified 
    = ts2;

            
    // if (MONITOR)
            
    // System.out.println("SLEEP COUNT: " + count);
        }

    看出来了,这段只是想计算时间的。为什么要这么计算时间呢?

    去FSDirectory类看到,他的方法就要简单很多

    代码1-10

       /// <summary>Set the modified time of an existing file to now. </summary>
            public override void  TouchFile(System.String name)
            {
                System.IO.FileInfo file 
    = new System.IO.FileInfo(System.IO.Path.Combine(directory.FullName, name));
                file.LastWriteTime 
    = System.DateTime.Now;
            }
     

    为什么在RAMDirectory需要这么来计算,那是为了取得一种随机数的效果,尽量不会产生重复的。因为内存操作文件,而且载入内存的一般文件比较小,对文件时间的更新如果是用DateTime.Now的话就会产生误差。通过这种算法,用Thread.Sleep的方式,然后通过循环对比,让两个时间产生差距,否则很可能会一样。要是一样,对文件版本的控制就不是很好。而这个睡眠0.01ms,速度很快,不会影响性能。实际上采用下面的方式也是一样的:


    代码1-11

     long ts2, ts1 = System.DateTime.Now.Ticks;
                
    do
                {
                    
    try
                    {

                        System.Threading.Thread.Sleep(
    new System.TimeSpan((System.Int64)10000 * 0 + 100 * 1));
                    }
                    
    catch (System.Threading.ThreadInterruptedException)
                    {
                    }
                    ts2 
    = System.DateTime.Now.Ticks;
                    
    //       if (MONITOR) {
                    
    //         count++;
                    
    //       }
                }
                
    while (ts1 == ts2);
     

    而new System.TimeSpan((System.Int64)10000 * 0 + 100 * 1)取0.01毫秒,那是因为Thread.Sleep的另外一个重载函数,只能放入int值,即使放1,也会是1毫秒,扩大了100倍的时间。经过我的测试,这个循环需要执行几千次,这里暂且不评论这个用法的好坏。


    其他方法都比较简单,很容易看懂,还要注意的就是一个MakeLock方法。转换工具导致了一个内嵌类,要分析这个方法的操作,还要看Java的源码。