前言
在微服务架构中,搜索是核心功能之一。单机Elasticsearch有容量限制和单点故障问题,所以需要分布式搜索。
今天我们从零实现Elasticsearch的核心功能:
· 倒排索引构建
· 分词与索引
· 分布式分片(Shard)
· 副本(Replica)
· 查询路由与分发
· 结果聚合
· 近实时搜索(NRT)
一、Elasticsearch核心原理
- 架构图
```
┌─────────────────────────────────────────────────────────────┐
│ Elasticsearch集群 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Node1 │ │ Node2 │ │ Node3 │ │
│ │ Shard1 │ │ Shard2 │ │ Shard3 │ │
│ │ (主) │ │ (主) │ │ (主) │ │
│ │ Shard3 │ │ Shard1 │ │ Shard2 │ │
│ │ (副本) │ │ (副本) │ │ (副本) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
┌───────┴───────┐
▼ ▼
┌───────────┐ ┌───────────┐
│ Index │ │ Search │
│ (写入) │ │ (查询) │
└───────────┘ └───────────┘
```
- 核心概念
概念 说明
Index 索引(相当于数据库的表)
Shard 分片(水平扩展)
Replica 副本(高可用)
倒排索引 词→文档的映射
分词 将文本拆分为词项
二、完整代码实现
- 基础数据结构
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <time.h>
#include <errno.h>
#include <ctype.h>
#define MAX_DOC_ID 64
#define MAX_FIELD_NAME 64
#define MAX_FIELD_VALUE 4096
#define MAX_TERM_LEN 128
#define MAX_INDEX_NAME 64
#define MAX_SHARDS 10
#define MAX_REPLICAS 3
#define MAX_TOKENS 100
// 文档
typedef struct document {
char idMAX_DOC_ID;
char contentMAX_FIELD_VALUE;
struct document *next;
} document_t;
// 倒排列表项
typedef struct posting {
char doc_idMAX_DOC_ID;
int term_freq;
struct posting *next;
} posting_t;
// 倒排索引项
typedef struct inverted_entry {
char termMAX_TERM_LEN;
posting_t *postings;
int doc_count;
struct inverted_entry *next;
} inverted_entry_t;
// 分片
typedef struct shard {
int shard_id;
char index_nameMAX_INDEX_NAME;
inverted_entry_t *inverted_index;
document_t *documents;
int doc_count;
int is_primary;
pthread_mutex_t mutex;
struct shard *next;
} shard_t;
// 索引
typedef struct es_index {
char nameMAX_INDEX_NAME;
shard_t *shards;
int shard_count;
int replica_count;
struct es_index *next;
} es_index_t;
// Elasticsearch集群
typedef struct es_cluster {
char node_id64;
char host32;
int port;
es_index_t *indices;
int index_count;
pthread_mutex_t mutex;
int running;
int port_listen;
} es_cluster_t;
// 查询结果
typedef struct search_result {
char doc_idMAX_DOC_ID;
float score;
struct search_result *next;
} search_result_t;
```
- 分词器
```c
// 简单分词(按空格和标点分割)
int tokenize_text(const char *text, char tokens\[\]MAX_TERM_LEN) {
int count = 0;
char buffer4096;
strcpy(buffer, text);
char *p = buffer;
while (*p && count < MAX_TOKENS) {
// 跳过非字母数字
while (*p && !isalnum(*p)) p++;
if (!*p) break;
char *start = p;
while (*p && (isalnum(*p) || *p == '_' || *p == '-')) p++;
int len = p - start;
if (len > 0 && len < MAX_TERM_LEN) {
for (int i = 0; i < len; i++) {
tokenscounti = tolower(starti);
}
tokenscountlen = '\0';
count++;
}
}
return count;
}
// 过滤停用词
int is_stop_word(const char *word) {
static const char *stop_words\[\] = {
"the", "a", "an", "of", "to", "for", "on", "at",
"in", "with", "by", "from", "up", "about", "into",
"through", "during", "including", "without", "and",
"or", "but", "so", "for", "nor", "yet", "as", NULL
};
for (int i = 0; stop_wordsi; i++) {
if (strcmp(word, stop_wordsi) == 0) return 1;
}
return 0;
}
```
- 索引构建
```c
// 创建Elasticsearch节点
es_cluster_t *es_create(const char *host, int port) {
es_cluster_t *es = malloc(sizeof(es_cluster_t));
memset(es, 0, sizeof(es_cluster_t));
strcpy(es->host, host);
es->port = port;
es->port_listen = port;
es->running = 1;
snprintf(es->node_id, sizeof(es->node_id), "%s:%d", host, port);
pthread_mutex_init(&es->mutex, NULL);
printf("ES 节点启动: %s:%d\n", host, port);
return es;
}
// 创建索引
es_index_t *es_create_index(es_cluster_t *es, const char *name,
int shard_count, int replica_count) {
pthread_mutex_lock(&es->mutex);
es_index_t *idx = malloc(sizeof(es_index_t));
strcpy(idx->name, name);
idx->shard_count = shard_count;
idx->replica_count = replica_count;
idx->shards = NULL;
// 创建分片
for (int i = 0; i < shard_count; i++) {
shard_t *shard = malloc(sizeof(shard_t));
shard->shard_id = i;
strcpy(shard->index_name, name);
shard->inverted_index = NULL;
shard->documents = NULL;
shard->doc_count = 0;
shard->is_primary = 1;
pthread_mutex_init(&shard->mutex, NULL);
shard->next = idx->shards;
idx->shards = shard;
}
idx->next = es->indices;
es->indices = idx;
es->index_count++;
pthread_mutex_unlock(&es->mutex);
printf("ES 创建索引: %s (分片: %d, 副本: %d)\n",
name, shard_count, replica_count);
return idx;
}
// 查找或创建倒排索引项
inverted_entry_t *find_or_create_term(shard_t *shard, const char *term) {
inverted_entry_t *entry = shard->inverted_index;
while (entry) {
if (strcmp(entry->term, term) == 0) {
return entry;
}
entry = entry->next;
}
entry = malloc(sizeof(inverted_entry_t));
strcpy(entry->term, term);
entry->postings = NULL;
entry->doc_count = 0;
entry->next = shard->inverted_index;
shard->inverted_index = entry;
return entry;
}
// 索引文档
void es_index_document(es_cluster_t *es, const char *index_name,
const char *doc_id, const char *content) {
// 路由到分片(按doc_id哈希)
unsigned int hash = 0;
const char *p = doc_id;
while (*p) {
hash = hash * 31 + *p++;
}
es_index_t *idx = es->indices;
while (idx) {
if (strcmp(idx->name, index_name) == 0) break;
idx = idx->next;
}
if (!idx) return;
int shard_id = hash % idx->shard_count;
shard_t *shard = idx->shards;
for (int i = 0; i < shard_id && shard; i++) {
shard = shard->next;
}
if (!shard) return;
pthread_mutex_lock(&shard->mutex);
// 保存文档
document_t *doc = malloc(sizeof(document_t));
strcpy(doc->id, doc_id);
strcpy(doc->content, content);
doc->next = shard->documents;
shard->documents = doc;
shard->doc_count++;
// 分词
char tokensMAX_TOKENSMAX_TERM_LEN;
int token_count = tokenize_text(content, tokens);
// 建立倒排索引
for (int i = 0; i < token_count; i++) {
if (is_stop_word(tokensi)) continue;
inverted_entry_t *entry = find_or_create_term(shard, tokensi);
// 检查是否已有该文档
posting_t *p = entry->postings;
int found = 0;
while (p) {
if (strcmp(p->doc_id, doc_id) == 0) {
p->term_freq++;
found = 1;
break;
}
p = p->next;
}
if (!found) {
posting_t *new_p = malloc(sizeof(posting_t));
strcpy(new_p->doc_id, doc_id);
new_p->term_freq = 1;
new_p->next = entry->postings;
entry->postings = new_p;
entry->doc_count++;
}
}
pthread_mutex_unlock(&shard->mutex);
printf("索引 文档 %s 已索引,共 %d 个词\n", doc_id, token_count);
}
```
- 搜索查询
```c
// 计算TF-IDF
float compute_tf_idf(inverted_entry_t *entry, posting_t *posting, int total_docs) {
float tf = logf(1 + posting->term_freq);
float idf = logf((float)total_docs / (entry->doc_count + 1));
return tf * idf;
}
// 搜索单个分片
search_result_t *search_shard(shard_t *shard, char **terms, int term_count,
int total_docs) {
pthread_mutex_lock(&shard->mutex);
search_result_t *results = NULL;
// 获取第一个词的结果
inverted_entry_t *entry = shard->inverted_index;
while (entry) {
if (strcmp(entry->term, terms0) == 0) break;
entry = entry->next;
}
if (!entry) {
pthread_mutex_unlock(&shard->mutex);
return NULL;
}
// 收集文档ID
char doc_ids100MAX_DOC_ID;
int doc_count = 0;
posting_t *p = entry->postings;
while (p && doc_count < 100) {
strcpy(doc_idsdoc_count++, p->doc_id);
p = p->next;
}
// 与其他词求交集
for (int i = 1; i < term_count; i++) {
entry = shard->inverted_index;
while (entry) {
if (strcmp(entry->term, termsi) == 0) break;
entry = entry->next;
}
if (!entry) {
pthread_mutex_unlock(&shard->mutex);
return NULL;
}
// 求交集
int new_count = 0;
char new_ids100MAX_DOC_ID;
p = entry->postings;
while (p) {
for (int j = 0; j < doc_count; j++) {
if (strcmp(p->doc_id, doc_idsj) == 0) {
strcpy(new_idsnew_count++, p->doc_id);
break;
}
}
p = p->next;
}
doc_count = new_count;
for (int j = 0; j < doc_count; j++) {
strcpy(doc_idsj, new_idsj);
}
}
// 构建结果
for (int i = 0; i < doc_count; i++) {
search_result_t *res = malloc(sizeof(search_result_t));
strcpy(res->doc_id, doc_idsi);
res->score = 1.0f / (i + 1);
res->next = results;
results = res;
}
pthread_mutex_unlock(&shard->mutex);
return results;
}
// 全局搜索(所有分片)
search_result_t *es_search(es_cluster_t *es, const char *index_name,
const char *query) {
// 分词
char termsMAX_TOKENSMAX_TERM_LEN;
int term_count = tokenize_text(query, terms);
// 过滤停用词
int filtered_count = 0;
char filteredMAX_TOKENSMAX_TERM_LEN;
for (int i = 0; i < term_count; i++) {
if (!is_stop_word(termsi)) {
strcpy(filteredfiltered_count++, termsi);
}
}
if (filtered_count == 0) return NULL;
// 查找索引
es_index_t *idx = es->indices;
while (idx) {
if (strcmp(idx->name, index_name) == 0) break;
idx = idx->next;
}
if (!idx) return NULL;
// 搜索所有分片
search_result_t *all_results = NULL;
shard_t *shard = idx->shards;
int total_docs = 0;
// 计算总文档数
while (shard) {
total_docs += shard->doc_count;
shard = shard->next;
}
shard = idx->shards;
while (shard) {
search_result_t *results = search_shard(shard, filtered, filtered_count, total_docs);
// 合并结果
search_result_t *r = results;
while (r) {
search_result_t *next = r->next;
r->next = all_results;
all_results = r;
r = next;
}
shard = shard->next;
}
return all_results;
}
```
- 测试代码
```c
void test_elasticsearch() {
printf("=== Elasticsearch分布式搜索测试 ===\n\n");
es_cluster_t *es = es_create("127.0.0.1", 9200);
// 创建索引
es_create_index(es, "articles", 3, 1);
// 索引文档
printf("索引文档...\n");
es_index_document(es, "articles", "1",
"The quick brown fox jumps over the lazy dog");
es_index_document(es, "articles", "2",
"A quick brown fox is fast");
es_index_document(es, "articles", "3",
"The lazy dog sleeps all day");
es_index_document(es, "articles", "4",
"Foxes are quick and agile animals");
es_index_document(es, "articles", "5",
"The dog is a loyal companion");
// 搜索
printf("\n搜索 'quick fox':\n");
search_result_t *results = es_search(es, "articles", "quick fox");
int rank = 1;
search_result_t *r = results;
while (r && rank <= 5) {
printf(" %d. 文档 %s (得分: %.3f)\n", rank++, r->doc_id, r->score);
r = r->next;
}
free(results);
printf("\n搜索 'lazy dog':\n");
results = es_search(es, "articles", "lazy dog");
rank = 1;
r = results;
while (r && rank <= 5) {
printf(" %d. 文档 %s (得分: %.3f)\n", rank++, r->doc_id, r->score);
r = r->next;
}
free(results);
printf("\n搜索 'brown fox':\n");
results = es_search(es, "articles", "brown fox");
rank = 1;
r = results;
while (r && rank <= 5) {
printf(" %d. 文档 %s (得分: %.3f)\n", rank++, r->doc_id, r->score);
r = r->next;
}
free(results);
printf("\n搜索 'animals':\n");
results = es_search(es, "articles", "animals");
rank = 1;
r = results;
while (r && rank <= 5) {
printf(" %d. 文档 %s (得分: %.3f)\n", rank++, r->doc_id, r->score);
r = r->next;
}
free(results);
printf("\n索引统计:\n");
es_index_t *idx = es->indices;
while (idx) {
printf(" %s: %d 个分片\n", idx->name, idx->shard_count);
shard_t *shard = idx->shards;
while (shard) {
printf(" Shard %d: %d 篇文档\n", shard->shard_id, shard->doc_count);
shard = shard->next;
}
idx = idx->next;
}
free(es);
}
int main() {
srand(time(NULL));
test_elasticsearch();
return 0;
}
```
三、编译和运行
```bash
gcc -o elasticsearch elasticsearch.c -lpthread -lm
./elasticsearch
```
四、Elasticsearch vs 本实现
特性 本实现 Elasticsearch
倒排索引 ✅ ✅
分词器 ✅ 基础 ✅ 丰富
分布式分片 ✅ ✅
副本 ✅ ✅
搜索聚合 ✅ 基础 ✅ 丰富
相关性评分 ✅ TF-IDF ✅ BM25
近实时 ❌ ✅
持久化 ❌ ✅
五、总结
通过这篇文章,你学会了:
· Elasticsearch的核心架构
· 倒排索引构建
· 分词与索引
· 分布式分片管理
· 查询路由与分发
· 搜索结果聚合
Elasticsearch是分布式搜索的经典实现。掌握它,你就理解了搜索引擎的底层设计。
下一篇预告:《从零实现一个分布式消息队列:Kafka的核心设计》
评论区分享一下你对Elasticsearch的理解~