sphinx 源码阅读之数据结构与算法

源码在 sphinx 官网上就可以下载到.
起初我下载的是最新版本,结果由于代码大约有 10W 行,我看了快 1W 行后发现这样看也不是个办法。
于是我想着生成一个项目关系图来阅读代码,但是我这电脑只有windows, 网上介绍的大多都是 linux 上的,于是我只好取消这个念头。
后来,我想我看sphinx源码主要是先弄明白 sphinx 的工作原理,而工作原理应该一直都是保持不变的,于是我就去下载第一个版本。
第一个版本果然给力,只有 1W 行,于是我就开始高高兴兴的开始从 main 函数开始看源代码了。
看了不就发现 sphinx 用了很多数据结构,而且是自己等装好的,还是先把这些数据结构弄明白了比较好。
于是就有了这篇文章。
为了方便读者阅读,这些数据结构和算法就从简单的慢慢罗列出来。

大家可以看右面的目录,然后去看自己感兴趣的数据结构或算法对应的小节。
如果对那个小节有疑问,可以随时留言。

sphinx 把最值封装成了一个宏。

  
  
  1. #define Min(a,b) ((a)<(b)?(a):(b))
  2. #define Max(a,b) ((a)>(b)?(a):(b))

为了这个通用,使用了基本的模板函数。
而交换则使用第三个缓存变量来实现这个功能。

  
  
  1. template<typename T>
  2. inline void Swap(T & v1, T & v2) {
  3. T temp = v1;
  4. v1 = v2;
  5. v2 = temp;
  6. }

这个 vector 实现的功能很简单,基本的 insert,remove,get, set 等操作。
只是附加了一个排序功能。
具体实现方式这里就不多说了,这些都是一个类基本的操作,都很容易实现(需要谁需要这个vector的实现讲解,可以留言)。

  
  
  1. template<typename T, int INITIAL_LIMIT = 1024>
  2. class CSphVector {
  3. public:
  4. CSphVector(); //初始化向量
  5. ~CSphVector(); //回收向量
  6. T & Add(); //增加一个元素,返回这个元素的引用
  7. void Add(const T & tValue);//增加一个元素
  8. T & Last();//得到最后一个元素
  9. void Remove(int iIndex);//删除指定位置的元素
  10. void Grow(int iNewLimit);//扩大缓存的大小,两倍两倍的增长
  11. void Resize(int iNewLength);// 原先设置数组的大小
  12. void Reset();// 重置数组
  13. int GetLength();//得到数组的长度
  14. void Sort(int iStart = 0, int iEnd = -1);// 正常排序
  15. void RSort(int iStart = 0, int iEnd = -1);// 逆序
  16. const T & operator [](int iIndex) const;// 读指定位置的值
  17. T & operator [](int iIndex);// 设置指定位置的值
  18. private:
  19. int m_iLength;//数组大小
  20. int m_iLimit;//数组缓存大小
  21. T * m_pData;//数组
  22. };

这次 sphinx 自己实现的 string 类的功能就比较多了。
这里我罗列出一些比较简单的功能。

  
  
  1. struct CSphString{
  2. CSphString (); //构造
  3. CSphString ( const char * sString );
  4. CSphString ( const CSphString & rhs );
  5. CSphString ( const char * sValue, int iLen );
  6. ~CSphString (); //析构
  7. const char * cstr () const; //得到字符串
  8. const char * scstr() const;//得到字符串,默认未空串
  9. inline bool operator == ( const char * t ) const; //判断两个串是否相等
  10. inline bool operator == ( const CSphString & t ) const;
  11. inline bool operator != ( const CSphString & t ) const;
  12. bool operator != ( const char * t ) const;
  13. const CSphString & operator = ( const CSphString & rhs );
  14. CSphString SubString ( int iStart, int iCount ) const;
  15. bool IsEmpty () const;
  16. CSphString & ToLower ();
  17. CSphString & ToUpper ();
  18. int Length () const;
  19. bool operator < ( const CSphString & b );
  20. };

判断一个字符是不是自己想要的字符。

  
  
  1. inline int sphIsAlpha ( int c ){
  2. return ( c>='0' && c<='9' ) || ( c>='a' && c<='z' ) || ( c>='A' && c<='Z' ) || c=='-' || c=='_';
  3. }

判断一个字符是不是空白

  
  
  1. inline bool sphIsSpace ( int iCode ){
  2. return iCode==' ' || iCode=='\t' || iCode=='\n' || iCode=='\r';
  3. }

字符串 trim 这个功能很常用,取出前边和后边的空白。

  
  
  1. static char * ltrim ( char * sLine ){
  2. while ( *sLine && isspace(*sLine) )
  3. sLine++;
  4. return sLine;
  5. }
  6. static char * rtrim ( char * sLine ){
  7. char * p = sLine + strlen(sLine) - 1;
  8. while ( p>=sLine && isspace(*p) )
  9. p--;
  10. p[1] = '\0';
  11. return sLine;
  12. }
  13. static char * trim ( char * sLine ){
  14. return ltrim ( rtrim ( sLine ) );
  15. }

切割字符串也是很常用的函数。
一般需要指定分隔符,默认分隔符是空白。
具体的实现代码这里就不展示了。

  
  
  1. void sphSplit ( CSphVector<CSphString> & dOut, const char * sIn, const char * sBounds ){
  2. if ( !sIn )return;
  3. const char * p = (char*)sIn;
  4. while ( *p ){
  5. // skip until the first non-boundary character
  6. const char * sNext = p;
  7. while ( *p && !strchr ( sBounds, *p ) )p++;
  8. // add the token, skip the char
  9. dOut.Add().SetBinary ( sNext, p-sNext );
  10. p++;
  11. }
  12. }

正则表达式大家都用过吧,这次 sphinx 实现了一个简单的正则表达式检验函数。
主要用于检验一个字符串是否符合指定的格式。

  
  
  1. bool sphWildcardMatch ( const char * sString, const char * sPattern ){
  2. if ( !sString || !sPattern )return false;
  3. const char * s = sString;
  4. const char * p = sPattern;
  5. while ( *s ){
  6. switch ( *p ){
  7. case '\\':
  8. // escaped char, strict match the next one literally
  9. p++;
  10. if ( *s++!=*p++ )return false;
  11. break;
  12. case '?':
  13. // match any character
  14. s++;
  15. p++;
  16. break;
  17. case '%':
  18. // gotta match either 0 or 1 characters
  19. // well, lets look ahead and see what we need to match next
  20. p++;
  21. // just a shortcut, %* can be folded to just *
  22. if ( *p=='*' )break;
  23. // plain char after a hash? check the non-ambiguous cases
  24. if ( !sphIsWild(*p) ){
  25. if ( s[0]!=*p ){
  26. // hash does not match 0 chars
  27. // check if we can match 1 char, or it's a no-match
  28. if ( s[1]!=*p )return false;
  29. s++;
  30. break;
  31. } else{
  32. // hash matches 0 chars
  33. // check if we could ambiguously match 1 char too, though
  34. if ( s[1]!=*p )break;
  35. // well, fall through to "scan both options" route
  36. }
  37. }
  38. // could not decide yet
  39. // so just recurse both options
  40. if ( sphWildcardMatch ( s, p ) )return true;
  41. if ( sphWildcardMatch ( s+1, p ) )return true;
  42. return false;
  43. case '*':
  44. // skip all the extra stars and question marks
  45. for ( p++; *p=='*' || *p=='?'; p++ )
  46. if ( *p=='?' ){
  47. s++;
  48. if ( !*s )return p[1]=='\0';
  49. }
  50. // short-circuit trailing star
  51. if ( !*p )return true;
  52. // so our wildcard expects a real character
  53. // scan forward for its occurrences and recurse
  54. for ( ;; ){
  55. if ( !*s )return false;
  56. if ( *s==*p && sphWildcardMatch ( s+1, p+1 ) )return true;
  57. s++;
  58. }
  59. break;
  60. default:
  61. // default case, strict match
  62. if ( *s++!=*p++ )return false;
  63. break;
  64. }
  65. }
  66. // string done
  67. // pattern should be either done too, or a trailing star, or a trailing hash
  68. return p[0]=='\0'|| ( p[0]=='*' && p[1]=='\0' )|| ( p[0]=='%' && p[1]=='\0' );
  69. }

做项目的时候经常会遇到一些打日志的库,其实这个功能很简单。
基本原理都是使用和 printf 类似的方法: 变参。

  
  
  1. static void StdoutLogger ( ESphLogLevel eLevel, const char * sFmt, va_list ap ){
  2. switch ( eLevel ){
  3. case SPH_LOG_FATAL: fprintf ( stdout, "FATAL: " ); break;
  4. case SPH_LOG_WARNING: fprintf ( stdout, "WARNING: " ); break;
  5. case SPH_LOG_INFO: fprintf ( stdout, "WARNING: " ); break;
  6. case SPH_LOG_DEBUG: fprintf ( stdout, "DEBUG: " ); break;
  7. }
  8. vfprintf ( stdout, sFmt, ap );
  9. fprintf ( stdout, "\n" );
  10. }
  11. static SphLogger_fn g_pLogger = &StdoutLogger;
  12. inline void Log ( ESphLogLevel eLevel, const char * sFmt, va_list ap ){
  13. if ( !g_pLogger ) return;
  14. ( *g_pLogger ) ( eLevel, sFmt, ap );
  15. }
  16. void sphWarning ( const char * sFmt, ... ){
  17. va_list ap;
  18. va_start ( ap, sFmt );
  19. Log ( SPH_LOG_WARNING, sFmt, ap );
  20. va_end ( ap );
  21. }
  22. void sphInfo ( const char * sFmt, ... );
  23. void sphLogFatal ( const char * sFmt, ... );
  24. void sphLogDebug ( const char * sFmt, ... );

上面的日志系统,最后还是调用了 vfprintf 函数, 没有让我们看到变参到底怎么实现的。
但是 sphinx 自己实现了一个 sphVSprintf 函数,和 vfprintf 类似,我不明白那个日志系统为什么不用自己的这个输出函数。
由于是对字符串分析,可以理解为一个简单的自动机。
遇到什么字符,期望下个字符是什么。
这里就不多说这个自动机了。

  
  
  1. static int sphVSprintf ( char * pOutput, const char * sFmt, va_list ap ){
  2. enum eStates { SNORMAL, SPERCENT, SHAVEFILL, SINWIDTH, SINPREC };
  3. eStates state = SNORMAL;
  4. int iPrec = 0;
  5. int iWidth = 0;
  6. char cFill = ' ';
  7. const char * pBegin = pOutput;
  8. bool bHeadingSpace = true;
  9. char c;
  10. while ( ( c = *sFmt++ )!=0 ){
  11. // handle percent
  12. if ( c=='%' ){
  13. if ( state==SNORMAL ){
  14. state = SPERCENT;
  15. iPrec = 0;
  16. iWidth = 0;
  17. cFill = ' ';
  18. } else{
  19. state = SNORMAL;
  20. *pOutput++ = c;
  21. }
  22. continue;
  23. }
  24. // handle regular chars
  25. if ( state==SNORMAL ){
  26. *pOutput++ = c;
  27. continue;
  28. }
  29. // handle modifiers
  30. switch ( c ){
  31. case '0':
  32. if ( state==SPERCENT ){
  33. cFill = '0';
  34. state = SHAVEFILL;
  35. break;
  36. }
  37. case '1': case '2': case '3':
  38. case '4': case '5': case '6':
  39. case '7': case '8': case '9':
  40. if ( state==SPERCENT || state==SHAVEFILL )
  41. {
  42. state = SINWIDTH;
  43. iWidth = c - '0';
  44. } else if ( state==SINWIDTH )
  45. iWidth = iWidth * 10 + c - '0';
  46. else if ( state==SINPREC )
  47. iPrec = iPrec * 10 + c - '0';
  48. break;
  49. case '-':
  50. if ( state==SPERCENT )
  51. bHeadingSpace = false;
  52. else
  53. state = SNORMAL; // FIXME? means that bad/unhandled syntax with dash will be just ignored
  54. break;
  55. case '.':
  56. state = SINPREC;
  57. iPrec = 0;
  58. break;
  59. case 's': // string
  60. {
  61. const char * pValue = va_arg ( ap, const char * );
  62. if ( !pValue )
  63. pValue = "(null)";
  64. int iValue = strlen ( pValue );
  65. if ( iWidth && bHeadingSpace )
  66. while ( iValue < iWidth-- )
  67. *pOutput++ = ' ';
  68. if ( iPrec && iPrec < iValue )
  69. while ( iPrec-- )
  70. *pOutput++ = *pValue++;
  71. else
  72. while ( *pValue )
  73. *pOutput++ = *pValue++;
  74. if ( iWidth && !bHeadingSpace )
  75. while ( iValue < iWidth-- )
  76. *pOutput++ = ' ';
  77. state = SNORMAL;
  78. break;
  79. }
  80. case 'p': // pointer
  81. {
  82. void * pValue = va_arg ( ap, void * );
  83. uint64_t uValue = uint64_t ( pValue );
  84. UItoA ( &pOutput, uValue, 16, iWidth, iPrec, cFill );
  85. state = SNORMAL;
  86. break;
  87. }
  88. case 'x': // hex integer
  89. case 'd': // decimal integer
  90. {
  91. DWORD uValue = va_arg ( ap, DWORD );
  92. UItoA ( &pOutput, uValue, ( c=='x' ) ? 16 : 10, iWidth, iPrec, cFill );
  93. state = SNORMAL;
  94. break;
  95. }
  96. case 'l': // decimal int64
  97. {
  98. int64_t iValue = va_arg ( ap, int64_t );
  99. UItoA ( &pOutput, iValue, 10, iWidth, iPrec, cFill );
  100. state = SNORMAL;
  101. break;
  102. }
  103. default:
  104. state = SNORMAL;
  105. *pOutput++ = c;
  106. }
  107. }
  108. // final zero to EOL
  109. *pOutput++ = '\n';
  110. return pOutput - pBegin;
  111. }

之前我曾写过一篇文章详解二进制数中1的个数,大家可以看看。

  
  
  1. inline int sphBitCount ( DWORD n ){
  2. register DWORD tmp;
  3. tmp = n - ((n >> 1) & 033333333333) - ((n >> 2) & 011111111111);
  4. return ( (tmp + (tmp >> 3) ) & 030707070707) % 63;
  5. }
  
  
  1. /// how much bits do we need for given int
  2. inline int sphLog2 ( uint64_t uValue )
  3. {
  4. #if USE_WINDOWS
  5. DWORD uRes;
  6. if ( BitScanReverse ( &uRes, (DWORD)( uValue>>32 ) ) )
  7. return 33+uRes;
  8. BitScanReverse ( &uRes, DWORD(uValue) );
  9. return 1+uRes;
  10. #elif __GNUC__ || __clang__
  11. if ( !uValue )
  12. return 0;
  13. return 64 - __builtin_clzl(uValue);
  14. #else
  15. int iBits = 0;
  16. while ( uValue )
  17. {
  18. uValue >>= 1;
  19. iBits++;
  20. }
  21. return iBits;
  22. #endif
  23. }

这个堆排序写的太奇葩了,哎,不能说什么了。

  
  
  1. /// generic accessor
  2. template < typename T > struct SphAccessor_T{
  3. T & Key ( T * a ) const; //得到指针的值
  4. void CopyKey ( T * pMed, T * pVal ) const;
  5. void Swap ( T * a, T * b ) const;
  6. T * Add ( T * p, int i ) const;//第i个位置的指针
  7. int Sub ( T * b, T * a ) const;//指针偏移量
  8. };
  9. /// heap sort helper
  10. // 自底向上进行堆排序
  11. //pData 带排序数组
  12. //iStart 开始位置
  13. //iEnd 结束位置
  14. //COMP 比较函数
  15. //ACC 访问指针的类
  16. template < typename T, typename U, typename V >
  17. void sphSiftDown ( T * pData, int iStart, int iEnd, U COMP, V ACC ){
  18. for ( ;; ){
  19. int iChild = iStart*2+1;
  20. if ( iChild>iEnd )return;
  21. int iChild1 = iChild+1;
  22. if ( iChild1<=iEnd && COMP.IsLess ( ACC.Key ( ACC.Add ( pData, iChild ) ), ACC.Key ( ACC.Add ( pData, iChild1 ) ) ) )
  23. iChild = iChild1;
  24. if ( COMP.IsLess ( ACC.Key ( ACC.Add ( pData, iChild ) ), ACC.Key ( ACC.Add ( pData, iStart ) ) ) )
  25. return;
  26. ACC.Swap ( ACC.Add ( pData, iChild ), ACC.Add ( pData, iStart ) );
  27. iStart = iChild;
  28. }
  29. }
  30. /// heap sort
  31. //奇葩的是先求出最大堆,然后反转,还边反转边维护堆。
  32. //最终是个最小堆。
  33. template < typename T, typename U, typename V >
  34. void sphHeapSort ( T * pData, int iCount, U COMP, V ACC ){
  35. if ( !pData || iCount<=1 )
  36. return;
  37. // build a max-heap, so that the largest element is root
  38. for ( int iStart=( iCount-2 )>>1; iStart>=0; iStart-- )
  39. sphSiftDown ( pData, iStart, iCount-1, COMP, ACC );
  40. // now keep popping root into the end of array
  41. for ( int iEnd=iCount-1; iEnd>0; ){
  42. ACC.Swap ( pData, ACC.Add ( pData, iEnd ) );
  43. sphSiftDown ( pData, 0, --iEnd, COMP, ACC );
  44. }
  45. }

sphinx 的快速排序也很奇葩。
一般的快速排序是递归,sphinx使用栈模拟递归。
这样栈的大小大概就是 log(n) 了。
而且栈为空的时候共有 log(n) 次。
当数据特殊的时候,快排会退化为 n\^2 的复杂度,这个时候,栈为空的几率变大了。
于是 sphinx 加了个修复, 当栈为空的次数大于 2.5 * log(n), 就是用上面那个奇葩的堆排序。
不过这个优化作用不大。

另外这个快排加了一个小优化:当需要排序的数量小于32时,使用插入排序。

  
  
  1. template < typename T, typename U, typename V >
  2. void sphSort ( T * pData, int iCount, U COMP, V ACC ){
  3. if ( iCount<2 )return;
  4. typedef T * P;
  5. // st0 and st1 are stacks with left and right bounds of array-part.
  6. // They allow us to avoid recursion in quicksort implementation.
  7. P st0[32], st1[32], a, b, i, j;
  8. typename V::MEDIAN_TYPE x;
  9. int k;
  10. const int SMALL_THRESH = 32;
  11. int iDepthLimit = sphLog2 ( iCount );
  12. iDepthLimit = ( ( iDepthLimit<<2 ) + iDepthLimit ) >> 1; // x2.5
  13. k = 1;
  14. st0[0] = pData;
  15. st1[0] = ACC.Add ( pData, iCount-1 );
  16. while ( k ){
  17. k--;
  18. i = a = st0[k];
  19. j = b = st1[k];
  20. // if quicksort fails on this data; switch to heapsort
  21. if ( !k ){
  22. if ( !--iDepthLimit ){
  23. sphHeapSort ( a, ACC.Sub ( b, a )+1, COMP, ACC );
  24. return;
  25. }
  26. }
  27. // for tiny arrays, switch to insertion sort
  28. int iLen = ACC.Sub ( b, a );
  29. if ( iLen<=SMALL_THRESH ){
  30. for ( i=ACC.Add ( a, 1 ); i<=b; i=ACC.Add ( i, 1 ) ){
  31. for ( j=i; j>a; ){
  32. P j1 = ACC.Add ( j, -1 );
  33. if ( COMP.IsLess ( ACC.Key(j1), ACC.Key(j) ) )
  34. break;
  35. ACC.Swap ( j, j1 );
  36. j = j1;
  37. }
  38. }
  39. continue;
  40. }
  41. // ATTENTION! This copy can lead to memleaks if your CopyKey
  42. // copies something which is not freed by objects destructor.
  43. ACC.CopyKey ( &x, ACC.Add ( a, iLen/2 ) );
  44. while ( a<b ){
  45. while ( i<=j ){
  46. while ( COMP.IsLess ( ACC.Key(i), x ) )
  47. i = ACC.Add ( i, 1 );
  48. while ( COMP.IsLess ( x, ACC.Key(j) ) )
  49. j = ACC.Add ( j, -1 );
  50. if ( i<=j ){
  51. ACC.Swap ( i, j );
  52. i = ACC.Add ( i, 1 );
  53. j = ACC.Add ( j, -1 );
  54. }
  55. }
  56. // Not so obvious optimization. We put smaller array-parts
  57. // to the top of stack. That reduces peak stack size.
  58. if ( ACC.Sub ( j, a )>=ACC.Sub ( b, i ) ){
  59. if ( a<j ) { st0[k] = a; st1[k] = j; k++; }
  60. a = i;
  61. } else{
  62. if ( i<b ) { st0[k] = i; st1[k] = b; k++; }
  63. b = j;
  64. }
  65. }
  66. }
  67. }

sphinx 的这个二分查找没有问题,但是和我们平常的二分查找还是有点不同的。
它的左右边界都是开放的,即(a,b).

  
  
  1. /// generic binary search
  2. template < typename T, typename U, typename PRED >
  3. T * sphBinarySearch ( T * pStart, T * pEnd, const PRED & tPred, U tRef ){
  4. if ( tPred(*pStart)==tRef )return pStart;
  5. if ( tPred(*pEnd)==tRef )return pEnd;
  6. while ( pEnd-pStart>1 ){
  7. if ( tRef<tPred(*pStart) || tPred(*pEnd)<tRef )break;
  8. T * pMid = pStart + (pEnd-pStart)/2;
  9. if ( tRef==tPred(*pMid) )return pMid;
  10. if ( tRef<tPred(*pMid) )pEnd = pMid;
  11. else pStart = pMid;
  12. }
  13. return NULL;
  14. }

要想去重,首先需要排序,所以这里假设容器是已经排完序的了。
然后假设 iDst 的上一个就是目前比较的值。
如果和上一个相等,则iSrc后移。
如果和上一个不相等,则找到一个新的值,将iDst位置置为新值,个数加1即可。

  
  
  1. /// generic uniq
  2. template < typename T, typename T_COUNTER >
  3. T_COUNTER sphUniq ( T * pData, T_COUNTER iCount ){
  4. if ( !iCount )return 0;
  5. T_COUNTER iSrc = 1, iDst = 1;
  6. while ( iSrc<iCount ){
  7. if ( pData[iDst-1]==pData[iSrc] )iSrc++;
  8. else pData[iDst++] = pData[iSrc++];
  9. }
  10. return iDst;
  11. }
Coreseek 全文检索服务器 2.0 (Sphinx 0.9.8)参考手册和源程序 手册內容: 文档版本:v0.9 目录 1. 简介 1.1. 什么是 Sphinx 1.2. Sphinx 的特性 1.3. 如何获得 Sphinx 1.4. 许可协议 1.5. 作者和贡献者 1.6. 开发历史 2. 安装 2.1. 支持的操作系统 2.2. 依赖的工具 2.3. 安装 Sphinx 2.4. 已知的问题和解决方法 2.5. Sphinx 快速入门教程 3. 建立索引 3.1. 数据源 3.2. 属性 3.3. 多值属性 ( MVA : multi-valued attributes) 3.4. 索引 3.5. 数据源的限制 3.6. 字符集 , 大小写转换 , 和转换表 3.7. SQL 数据源 (MySQL, PostgreSQL) 3.8. xmlpipe 数据源 3.9. xmlpipe2 数据源 3.10. 实时索引 更新 3.11. 索引合并 4. 搜索 4.1. 匹配模式 4.2. 布尔查询 4.3. 扩展查询 4.4. 权值计算 4.5. 排序模式 4.6. 结果分组(聚类) 4.7. 分布式搜索 4.8. searchd 日志格式 5. API 参考 5.1. 通用 API 方法 5.1.1. GetLastError 5.1.2. GetLastWarning 5.1.3. SetServer 5.1.4. SetRetries 5.1.5. SetArrayResult 5.2. 通用搜索设置 5.2.1. SetLimits 5.2.2. SetMaxQueryTime 5.3. 全文搜索设置 5.3.1. SetMatchMode 5.3.2. SetRankingMode 5.3.3. SetSortMode 5.3.4. SetWeights 5.3.5. SetFieldWeights 5.3.6. SetIndexWeights 5.4. 结果集过滤设置 5.4.1. SetIDRange 5.4.2. SetFilter 5.4.3. SetFilterRange 5.4.4. SetFilterFloatRange 5.4.5. SetGeoAnchor 5.5. GROUP BY 设置 5.5.1. SetGroupBy 5.5.2. SetGroupDistinct 5.6. 搜索 5.6.1. Query 5.6.2. AddQuery 5.6.3. RunQueries 5.6.4. ResetFilters 5.6.5. ResetGroupBy 5.7. 额外的方法 5.7.1. BuildExcerpts 5.7.2. UpdateAttributes 6. MySQL 存储引擎 (SphinxSE) 6.1. SphinxSE 概览 6.2. 安装 SphinxSE 6.2.1. 在 MySQL 5.0.x 上 编译 SphinxSE 6.2.2. 在 MySQL 5.1.x 上编译 SphinxSE 6.2.3. SphinxSE 安装测试 6.3. 使用 SphinxSE 7. 报告 bugs 8. sphinx.conf 选项参考 8.1. Data source 配置选项 8.1.1. type 8.1.2. sql_host 8.1.3. sql_port 8.1.4. sql_user 8.1.5. sql_
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值