實現比對文字相似性查詢

RiCo 技術農場
RiCosNote
Published in
15 min readMar 28, 2024

--

目前RAG( Retrieval Augmented Generation)可說是LLM標配,OpenAI也有相關embedding models,可將文字轉換為多維向量值,這時可用數學運算評估文字相似性,實現推薦系統、文字檢索、領域分類...等。

我的簡易相似性查詢處理大致如下

使用者新增來源資料->Website->呼叫OpenAI的embeddings API取文字轉換後的多維向量值(1536)->多維向量值儲存至Pgvector。

使用者輸入查詢文字->Website->呼叫OpenAI的embeddings API取文字轉換後的多維向量值(1536)->查詢Pgvector並依據輸入向量返回最相似資料。

測試來源資料

良好的註釋是代碼可讀性的關鍵。
風險評估是制定保單方案的核心。
清洗和預處理數據是獲得準確模型的重要步驟。
風險管理是確保投資安全的重要措施之一。
個性化學習是提高教學效果的關鍵策略之一。
內容創作的多樣性是吸引觀眾的關鍵。
模組化設計有助於代碼的可維護性和重用性。
理賠流程的優化可以提高客戶滿意度。
開放原始碼對於電玩業來說是一個重要的發展方向,能夠促進遊戲社區的共享和合作。
環保意識日益增強,電玩公司應該考慮在遊戲開發過程中採取環保措施,減少對環境的影響。
綠能技術在電玩硬體設計中的應用可以降低能源消耗,並減少對自然資源的需求。
電玩業在採用LLM技術方面有著巨大的潛力,可以提供更加生動和真實的遊戲體驗。

pgvector向量資料庫

pgvector是PostgreSQL的擴充,PostgreSQL開源超過35年,一個強大的RDBMS,也支援向量。

來源資料(mp3、mp4、csv、pdf..等)全以向量形式存在向量資料庫中,透過向量之間的度量,實現高效的相似度搜尋和分析。

查詢符號

<->:計算兩個向量之間的歐幾里得距離,表示兩個向量點之間的距離,較小的距離表示兩個向量相似性越大。

<=>:計算兩個向量之間的余弦相似度,比較兩個向量的方向而非大小,範圍-1~1之間,1表示向量相同,0表示無關,-1表示相反方向。

<#>:計算兩個向量之間的曼哈頓距離,表示每個維度對應座標差的絕對值之和,相對歐幾里得距離,曼哈頓距離強調沿維度的最小移動。

我使用docker compose 快速啟動pgvector

services:
db:
hostname: db
image: ankane/pgvector
ports:
- 5432:5432
environment:
- POSTGRES_DB=vectordb
- POSTGRES_USER=testuser
- POSTGRES_PASSWORD=testpwd
- POSTGRES_HOST_AUTH_METHOD=trust

容器啟動後輸入以下SQL(建立擴充和資料表)

CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE IF NOT EXISTS embeddings (
id SERIAL PRIMARY KEY,
embedding vector(1536),
text text,
created_at timestamptz DEFAULT now()
);

ASP.NET Core MVC加入相關packages

HigLabo.OpenAI

Pgvector

測試程式碼如下(demo就隨性了)

Controller

 string connstr="Host=127.0.0.1;Port=5432;Username=testuser;Password=testpwd;Database=vectordb;";

//查詢
[HttpGet]
public async Task<JsonResult> Query([FromQuery] string input)
{
var modelName = "text-embedding-3-small"; //text-embedding-3-large text-embedding-ada-002
var response = await _apiClient.EmbeddingsAsync(input
, modelName);
var model = new JsonResponseViewModel();
model.ResponseCode = 1;

var sb = new StringBuilder();
_npgsqlConnection = _npgsqlDataSource.OpenConnection();
await using (_npgsqlConnection)
await using (var cmd = new NpgsqlCommand("SELECT id,TEXT,embedding FROM embeddings ORDER BY embedding <=> @n1 LIMIT 3", _npgsqlConnection))
{
var floats = response.Data[0].Embedding.Select(c => float.Parse(c.ToString()));
var embedding = new Vector(floats.ToArray());
cmd.Parameters.AddWithValue("n1", embedding);
await using (var reader = await cmd.ExecuteReaderAsync())
{
if (!reader.HasRows)
{
sb.AppendLine($"Id:0 , text:no data , similarity:0 .");
}
else
{
while (await reader.ReadAsync())
{
var id = reader.GetValue(0).ToString();
var text = reader.GetValue(1).ToString();
var embedds = reader.GetValue(2).ToString().Trim('[', ']').Split(',');
var similarity = CalculateCosineSimilarity(floats.ToArray(), embedds.Select(c => float.Parse(c.Trim())).ToArray());
sb.AppendLine($"Id:{id} , text:{text} , similarity:{similarity} . ");
}
}
}
}

model.ResponseMessage = sb.ToString();
return Json(model);
}

private static float CalculateCosineSimilarity(float[] embedding1, float[] embedding2)
{
if (embedding1.Length != embedding2.Length)
{
throw new ArgumentException("Embeddings must have the same dimensionality.");
}

// 計算向量內積
var dotProduct = DotProduct(embedding1, embedding2);

// 計算向量長度
var magnitude1 = Magnitude(embedding1);
var magnitude2 = Magnitude(embedding2);

// 餘弦相似度公式:dotProduct / (magnitude1 * magnitude2)
if (magnitude1 == 0 || magnitude2 == 0)
{
return 0; // 避免除以零
}
else
{
return (float)(dotProduct / (magnitude1 * magnitude2));
}
}

// 計算向量內積
private static float DotProduct(float[] vector1, float[] vector2)
{
return vector1.Zip(vector2, (a, b) => a * b).Sum();
}

// 計算向量長度
private static double Magnitude(float[] vector)
{
return Math.Sqrt(vector.Select(x => x * x).Sum());
}

//新增來源資料
[HttpPost]
public async Task<JsonResult> Execute(string mytext)
{
//https://platform.openai.com/docs/guides/embeddings/embedding-models
var modelName = "text-embedding-3-small"; //text-embedding-3-large text-embedding-ada-002
var response = await _apiClient.EmbeddingsAsync(mytext
, modelName);
var model = new JsonResponseViewModel();
model.ResponseCode = 1;
model.ResponseMessage = JsonSerializer.Serialize(response.Data);

_npgsqlConnection = _npgsqlDataSource.OpenConnection();
await using (_npgsqlConnection)
await using (var cmd = new NpgsqlCommand("INSERT INTO embeddings (embedding,text) VALUES (@n1,@n2)", _npgsqlConnection))
{
var floats = response.Data[0].Embedding.Select(c => float.Parse(c.ToString()));
var embedding = new Vector(floats.ToArray());
cmd.Parameters.AddWithValue("n1", embedding);
cmd.Parameters.AddWithValue("n2", mytext);
await cmd.ExecuteNonQueryAsync();
}
return Json(model);
}

View:

Index.cshtml
<div class="container">

<div class="row p-1">
<div class="col-1">請輸入</div>
<div class="col-5"><input type="text" class="w-100" id="messageInput" /></div>
</div>
<div class="row p-1">
<div class="col-6 text-end">
<input type="button" id="sendButton" value="送出" class="btn btn-primary" />
<a href="@Url.Action("QueryView", "Embedding")" class="btn btn-primary">查詢頁</a>
</div>
</div>

<div class="row p-1">
<span id="result"></span>
</div>
<div class="spinner-border" id="spinner" role="status" aria-hidden="true">
<span class="visually-hidden">Loading...</span>
</div>
</div>


@section Scripts {
<script src="~/js/embedding.js"></script>

}

Query.cshtml
<div class="container">

<div class="row p-1">
<div class="col-1">請輸入</div>
<div class="col-5"><input type="text" class="w-100" id="queryInput"/></div>
</div>
<div class="row p-1">
<div class="col-6 text-end">
<input type="button" id="queryButton" value="查詢相似" class="btn btn-primary" />
</div>
</div>

<div class="row p-1">
<span id="queryresult"></span>
</div>
<div class="spinner-border" id="spinner" role="status" aria-hidden="true">
<span class="visually-hidden">Loading...</span>
</div>
</div>


@section Scripts {
<script src="~/js/embedding.js"></script>

}
Index.cshtml
Query.cshtml

Demo:

--

--

RiCo 技術農場
RiCosNote

分享工作上實戰經驗,如SQL Server,NetCore,C#,WEBApi,Kafka,Azure…等,Architect,Software Engineer, Technical Manger,Writer and Speaker