本文目录导读:

在PHP项目中实现相似性搜索,通常有几种不同的方法,具体选择取决于你的数据规模、精度要求和性能需求,以下是几种常见方案的详细介绍:
基于文本相似度算法
1 使用相似度函数
<?php
// Levenshtein 距离(编辑距离)
$word1 = "apple";
$word2 = "appl";
$distance = levenshtein($word1, $word2);
// 距离越小越相似,0表示完全相同
// similar_text(基于字符匹配)
$similarity = similar_text($word1, $word2, $percent);
// $percent 是相似度百分比
// 自定义 Jaccard 相似度
function jaccardSimilarity($str1, $str2) {
$set1 = array_unique(str_split($str1));
$set2 = array_unique(str_split($str2));
$intersection = array_intersect($set1, $set2);
$union = array_unique(array_merge($set1, $set2));
return count($intersection) / count($union);
}
?>
使用全文搜索引擎
1 Elasticsearch(推荐方案)
安装客户端:
composer require elasticsearch/elasticsearch
PHP实现:
<?php
require 'vendor/autoload.php';
use Elastic\Elasticsearch\ClientBuilder;
class SimilaritySearch {
private $client;
public function __construct() {
$this->client = ClientBuilder::create()
->setHosts(['localhost:9200'])
->build();
}
// 创建索引并设置自定义相似度
public function createIndex($indexName) {
$params = [
'index' => $indexName,
'body' => [
'settings' => [
'similarity' => [
'custom_similarity' => [
'type' => 'BM25',
'k1' => 1.2,
'b' => 0.75
]
]
],
'mappings' => [
'properties' => [
'title' => [
'type' => 'text',
'similarity' => 'custom_similarity'
],
'content' => [
'type' => 'text'
]
]
]
]
];
return $this->client->indices()->create($params);
}
// 执行相似性搜索
public function search($index, $query) {
$params = [
'index' => $index,
'body' => [
'query' => [
'more_like_this' => [
'fields' => ['title', 'content'],
'like' => $query,
'min_term_freq' => 1,
'max_query_terms' => 12
]
],
'size' => 10
]
];
return $this->client->search($params);
}
}
?>
向量相似度搜索(高级方案)
1 使用词向量(Word Embeddings)
<?php
class VectorSimilarity {
// 余弦相似度计算
public function cosineSimilarity($vectorA, $vectorB) {
$dotProduct = 0;
$normA = 0;
$normB = 0;
foreach ($vectorA as $i => $value) {
$dotProduct += $value * $vectorB[$i];
$normA += $value * $value;
$normB += $vectorB[$i] * $vectorB[$i];
}
if ($normA == 0 || $normB == 0) return 0;
return $dotProduct / (sqrt($normA) * sqrt($normB));
}
// 简单词袋模型向量化
public function vectorize($text, $vocabulary) {
$words = str_word_count(strtolower($text), 1);
$vector = array_fill(0, count($vocabulary), 0);
foreach ($words as $word) {
if (isset($vocabulary[$word])) {
$vector[$vocabulary[$word]]++;
}
}
return $vector;
}
}
// 使用示例
$vectorSimilarity = new VectorSimilarity();
// 构建词汇表
$vocabulary = [
'apple' => 0,
'orange' => 1,
'fruit' => 2,
'juice' => 3
];
$text1 = "apple fruit juice";
$text2 = "orange juice";
$vector1 = $vectorSimilarity->vectorize($text1, $vocabulary);
$vector2 = $vectorSimilarity->vectorize($text2, $vocabulary);
$similarity = $vectorSimilarity->cosineSimilarity($vector1, $vector2);
echo "相似度: " . $similarity;
?>
使用专用相似度搜索库
1 安装 Meilisearch
composer require meilisearch/meilisearch-php
<?php
require 'vendor/autoload.php';
use Meilisearch\Client;
class MeiliSearchService {
private $client;
public function __construct() {
$this->client = new Client('http://localhost:7700', 'masterKey');
}
// 添加文档
public function addDocuments($index, $documents) {
return $this->client->index($index)->addDocuments($documents);
}
// 相似性搜索(使用模糊匹配)
public function search($index, $query) {
return $this->client->index($index)->search($query, [
'attributesToSearchOn' => ['title', 'description'],
'showRankingScore' => true,
'fuzzy' => true
]);
}
}
?>
基于 MySQL 的近似搜索
<?php
class MySQLSimilaritySearch {
private $pdo;
public function __construct() {
$this->pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'password');
}
// 创建全文索引
public function createFulltextIndex($table, $columns) {
$columns = implode(', ', $columns);
$sql = "ALTER TABLE $table ADD FULLTEXT INDEX ft_index ($columns)";
$this->pdo->exec($sql);
}
// 使用全文搜索进行相似度匹配
public function searchSimilar($table, $query, $limit = 10) {
$sql = "SELECT *, MATCH(title, content) AGAINST(:query IN NATURAL LANGUAGE MODE) AS relevance
FROM $table
WHERE MATCH(title, content) AGAINST(:query IN NATURAL LANGUAGE MODE)
ORDER BY relevance DESC
LIMIT :limit";
$stmt = $this->pdo->prepare($sql);
$stmt->execute(['query' => $query, 'limit' => $limit]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
?>
完整示例:基于 TF-IDF 的相似度系统
<?php
class TfIdfSimilarity {
private $documents = [];
private $idfValues = [];
public function addDocument($id, $text) {
$words = $this->tokenize($text);
$this->documents[$id] = $words;
}
private function tokenize($text) {
// 简单的分词处理
$text = strtolower($text);
$words = str_word_count($text, 1);
// 去除停用词
$stopWords = ['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for'];
return array_diff($words, $stopWords);
}
public function calculateIdf() {
$totalDocs = count($this->documents);
// 计算每个词的文档频率
$docFrequency = [];
foreach ($this->documents as $words) {
$uniqueWords = array_unique($words);
foreach ($uniqueWords as $word) {
if (!isset($docFrequency[$word])) {
$docFrequency[$word] = 0;
}
$docFrequency[$word]++;
}
}
// 计算 IDF
foreach ($docFrequency as $word => $freq) {
$this->idfValues[$word] = log($totalDocs / ($freq + 1));
}
}
public function calculateTfIdf($words) {
$tfIdf = [];
$wordCount = count($words);
foreach ($words as $word) {
// 计算 TF
$tf = array_count_values($words)[$word] / $wordCount;
// 计算 TF-IDF
$idf = isset($this->idfValues[$word]) ? $this->idfValues[$word] : 0;
$tfIdf[$word] = $tf * $idf;
}
return $tfIdf;
}
public function search($query) {
$queryWords = $this->tokenize($query);
$queryVector = $this->calculateTfIdf($queryWords);
$scores = [];
foreach ($this->documents as $docId => $docWords) {
$docVector = $this->calculateTfIdf($docWords);
// 计算余弦相似度
$similarity = $this->cosineSimilarity($queryVector, $docVector);
$scores[$docId] = $similarity;
}
// 按相似度排序
arsort($scores);
return $scores;
}
private function cosineSimilarity($vectorA, $vectorB) {
$dotProduct = 0;
$normA = 0;
$normB = 0;
$allKeys = array_unique(array_merge(array_keys($vectorA), array_keys($vectorB)));
foreach ($allKeys as $key) {
$a = isset($vectorA[$key]) ? $vectorA[$key] : 0;
$b = isset($vectorB[$key]) ? $vectorB[$key] : 0;
$dotProduct += $a * $b;
$normA += $a * $a;
$normB += $b * $b;
}
if ($normA == 0 || $normB == 0) return 0;
return $dotProduct / (sqrt($normA) * sqrt($normB));
}
}
// 使用示例
$tfIdf = new TfIdfSimilarity();
$tfIdf->addDocument(1, "PHP is a popular programming language for web development");
$tfIdf->addDocument(2, "JavaScript is used for both frontend and backend development");
$tfIdf->addDocument(3, "Python excels in data science and machine learning");
$tfIdf->calculateIdf();
$results = $tfIdf->search("web development language");
foreach ($results as $docId => $score) {
echo "Document $docId: $score\n";
}
?>
选择建议
| 数据规模 | 精度要求 | 推荐方案 |
|---|---|---|
| 小(<1000条) | 一般 | MySQL全文搜索或PHP内置函数 |
| 中(<10万条) | 较高 | Elasticsearch或Meilisearch |
| 大(>10万条) | 很高 | 向量数据库(Milvus/Pinecone)+ Embeddings |
| 实时搜索 | 高 | Elasticsearch/Meilisearch |
性能优化建议
- 缓存计算结果 - 对常见的查询结果进行缓存
- 限制搜索范围 - 使用分页和限制返回结果数量
- 异步处理 - 对于大规模数据处理,使用消息队列
- 索引优化 - 定期重建索引,优化数据存储结构
选择哪种方案主要取决于你的具体需求:数据量大小、实时性要求、硬件资源和开发团队的技术栈,对于大多数PHP项目,Elasticsearch是最推荐的方案。