在 Elasticsearch 中使用用戶行為分析:使用 UBI 和 search-ui 創建一個應用程序

作者:來自 Elastic?Eduard Martin?及?Alexander Dávila

通過一個實際示例學習如何在 Elasticsearch 中使用 UBI。我們將創建一個在搜索和點擊結果時生成 UBI 事件的應用程序。

想要獲得 Elastic 認證嗎?看看下一次 Elasticsearch Engineer 培訓什么時候開始!

Elasticsearch 擁有豐富的新功能,能幫助你為自己的用例構建最佳的搜索解決方案。深入了解我們的示例筆記本,開始免費云試用,或者現在就在本地機器上嘗試 Elastic。


在本文中,我們將創建一個示例應用來收集用戶行為數據,展示如何將 UBI 擴展集成到 search-ui 中。我們還將自定義收集的數據,以展示 UBI 標準的靈活性,以及它如何滿足不同的需求。

這個示例應用是一個簡單的圖書搜索引擎,目標是能夠捕捉用戶的事件,并基于他們的行為(如搜索和點擊)將其索引到 Elasticsearch 中。

需求

這個應用需要在 Elasticsearch 中安裝 UBI 插件。你可以閱讀我們的博客文章獲取更多信息。

加載示例數據

我們需要先在 Elasticsearch 中準備一些數據。在 Kibana DevTools Console 中運行以下命令來加載一組產品列表,以便在我們的 UI 中展示。這將創建一個名為 “books” 的新索引,用于本示例。

POST /_bulk
{ "index" : { "_index" : "books" } }
{"name": "Snow Crash", "author": "Neal Stephenson", "release_date": "1992-06-01", "page_count": 470, "price": 14.99, "url": "https://www.amazon.com/Snow-Crash-Neal-Stephenson/dp/0553380958/", "image_url": "https://m.media-amazon.com/images/I/81p4Y+0HzbL._SY522_.jpg" }
{ "index" : { "_index" : "books" } }
{"name": "Revelation Space", "author": "Alastair Reynolds", "release_date": "2000-03-15", "page_count": 585, "price": 16.99, "url": "https://www.amazon.com/Revelation-Space-Alastair-Reynolds/dp/0441009425/", "image_url": "https://m.media-amazon.com/images/I/61nC2ExeTvL._SY522_.jpg"}
{ "index" : { "_index" : "books" } }
{"name": "1984", "author": "George Orwell", "release_date": "1985-06-01", "page_count": 328, "price": 12.99, "url": "https://www.amazon.com/1984-Signet-Classics-George-Orwell/dp/0451524934/", "image_url": "https://m.media-amazon.com/images/I/71rpa1-kyvL._SY522_.jpg"}
{ "index" : { "_index" : "books" } }
{"name": "Fahrenheit 451", "author": "Ray Bradbury", "release_date": "1953-10-15", "page_count": 227, "price": 11.99, "url": "https://www.amazon.com/Fahrenheit-451-Ray-Bradbury/dp/1451673310/", "image_url": "https://m.media-amazon.com/images/I/61sKsbPb5GL._SY522_.jpg"}
{ "index" : { "_index" : "books" } }
{"name": "Brave New World", "author": "Aldous Huxley", "release_date": "1932-06-01", "page_count": 268, "price": 12.99, "url": "https://www.amazon.com/Brave-New-World-Aldous-Huxley/dp/0060850523/", "image_url": "https://m.media-amazon.com/images/I/71GNqqXuN3L._SY522_.jpg"}
{ "index" : { "_index" : "books" } }
{"name": "The Handmaid's Tale", "author": "Margaret Atwood", "release_date": "1985-06-01", "page_count": 311, "price": 13.99, "url": "https://www.amazon.com/Handmaids-Tale-Margaret-Atwood/dp/038549081X/", "image_url": "https://m.media-amazon.com/images/I/61su39k8NUL._SY522_.jpg"}

創建示例應用程序

我們將使用 search-ui 創建一個 UI 應用程序,將 UBI 事件發送到 Elasticsearch。search-ui 是 Elastic 的 JavaScript 庫,用于使用內置的 React 組件創建 UI。

Search UI 是 Elastic 基于 React 的框架,用于構建搜索應用程序。它為搜索體驗中的所有關鍵部分提供組件 —— 例如搜索欄、分面、分頁和自動建議。自定義其行為(包括添加 UBI)非常簡單。

Elasticsearch 連接器

首先,我們需要安裝 Elasticsearch 連接器,步驟參考連接器教程。

1)從 GitHub 下載 search-ui 啟動應用:

curl https://codeload.github.com/elastic/app-search-reference-ui-react/tar.gz/master | tar -xz

2)進入新目錄 app-search-reference-ui-react-main 并安裝依賴項:

cd app-search-reference-ui-react-main
npm install

3)通過 npm 包管理器安裝 Elasticsearch 連接器:

npm install @elastic/search-ui-elasticsearch-connector

后端服務器

為了遵循最佳實踐并確保對 Elasticsearch 的調用通過中間層服務完成,我們來創建一個后端來調用我們的連接器:

1)我們先創建一個新目錄和一個新的 JavaScript 文件:

mkdir server
touch server/index.js

2)在新的 index.js 文件中,寫入:

import express from "express";
import ElasticsearchAPIConnector from "@elastic/search-ui-elasticsearch-connector";
import { Client } from "@elastic/elasticsearch";
import "dotenv/config";const app = express();
app.use(express.json());const connector = new ElasticsearchAPIConnector({host: process.env.ELASTICSEARCH_HOST,index: process.env.ELASTICSEARCH_INDEX,apiKey: process.env.ELASTICSEARCH_API_KEY
});const esClient = new Client({node: process.env.ELASTICSEARCH_HOST,auth: {apiKey: process.env.ELASTICSEARCH_API_KEY,},
});app.post("/api/search", async (req, res) => {const { state, queryConfig } = req.body;const response = await connector.onSearch(state, queryConfig);res.json(response);
});app.post("/api/autocomplete", async (req, res) => {const { state, queryConfig } = req.body;const response = await connector.onAutocomplete(state, queryConfig);res.json(response);
});app.post("/api/analytics", async (req, res, next) => {try {console.log(`Sending analytics for query_id: ${req.body.query_id}`)req.body.client_id = clientId;await esClient.index({index: "ubi_events",body: req.body,});console.log(req.body);res.status(200).json({ message: "Analytics event saved successfully" });} catch (error) {next(error);}
});app.listen(3001);

通過此更改,我們將默認行為(從瀏覽器調用 Elasticsearch)替換為調用我們的后端。這種方法更適合生產環境。

在文件末尾,將 export default function 替換為以下定義:

export default function App() {return (<SearchProvider config={config}><Layoutheader={<SearchBox autocompleteSuggestions={false} />}bodyContent={<ResultstitleField={"author"}urlField={"url"}thumbnailField={"image_url"}shouldTrackClickThrough={true}/>}/></SearchProvider>);
}

這將允許我們顯示圖書的圖片,并提供可點擊的鏈接。

要查看完整教程,請訪問此文檔。

按照步驟操作后,你將得到一個客戶端的 index.js 文件和一個服務器端的 server/index.js 相關文件。

配置連接器

我們將配置 onSearch 和 onResultClick 處理程序來設置 UBI query_id。然后,在執行搜索和點擊結果時發送 UBI 事件。

配置 onSearch:攔截搜索請求,為每個請求使用 UUID v4 分配一個唯一的 requestId,然后將請求傳遞給處理鏈中的下一個處理程序。我們將使用此 ID 作為 UBI query_id,用于將搜索和點擊分組。

進入 server/index.js 文件,并擴展連接器以配置 onSearch 方法:

const clientId = uuidv4(); // to maintain a constant client id
class UBIConnector extends ElasticsearchAPIConnector {async onSearch(requestState, queryConfig) {const result = await super.onSearch(requestState, queryConfig);result.requestId = uuidv4();result.clientId = clientId;return result;}
}

之后,聲明連接器并自定義搜索請求,通過 ext.ubi 搜索參數將生成的 ID 發送到 UBI 插件。

const connector = new UBIConnector({host: process.env.ELASTICSEARCH_HOST,index: process.env.ELASTICSEARCH_INDEX,apiKey: process.env.ELASTICSEARCH_API_KEY,},(requestBody, requestState, queryConfig) => {requestBody.ext = {ubi: {query_id: requestState.requestId,client_id: requestState.clientId || clientId,user_query: requestState.searchTerm || "",},};if (!requestState.searchTerm) return requestBody;requestBody.query = {multi_match: {query: requestState.searchTerm,fields: Object.keys(queryConfig.search_fields),},};return requestBody;}
);

別忘了添加新的導入。此外,由于我們的前端運行在 localhost:3000,而后端運行在 localhost:3001,它們被視為不同的源。‘源’ 由協議、域名和端口的組合定義,所以即使它們都在同一主機上并使用 HTTP 協議,不同的端口也會使它們成為不同的源,因此我們需要 CORS。要了解更多關于 CORS 的信息,請訪問本指南。

import cors from "cors";
import { v4 as uuidv4 } from "uuid";
...
app.use(cors({origin: "http://localhost:3000", // Your React app URLcredentials: true
}));

進入客戶端的 client/App.js 文件(點擊以打開完整的完成文件)。

在 config 對象聲明中添加 onResultClick 事件處理程序,每當用戶點擊搜索結果時,將分析數據發送到后端,捕獲的信息包括查詢 ID、結果詳情以及用戶交互的具體信息,如點擊的文檔屬性、文檔位置和頁碼。在這里,你還可以添加用戶同意共享的其他信息。確保遵守隱私法律(例如歐洲的 GDPR)。

const config = {apiConnector: connector,
onResultClick: async (r) => {const locationData = await getLocationData();const payload = {application: "search-ui",action_name: "click",query_id: r.requestId || "",client_id: r.clientId || "",timestamp: new Date().toISOString(),message_type: "CLICK_THROUGH",message: `Clicked ${r.result.name.raw}`,user_query: r.query,event_attributes: {object: {device: getDeviceType(),object_id: r.result.id.raw,description: `${r.result.name.raw}(${r.result.release_date.raw}) by ${r.result.author.raw}`,position: {ordinal: r.resultIndexOnPage,page_depth: r.page,},user: {ip: locationData.ip,city: locationData.city,region: locationData.region,country: locationData.country,location: {lat:locationData.latitude,lon:locationData.longitude}}},},};fetch(`http://localhost:3001/api/analytics`, {method: "POST",headers: {"Content-Type": "application/json",},body: JSON.stringify(payload),}).then((r) => console.log(r)).catch((error) => {console.error("Error:", error);});}// other Search UI config options
};

SearchUI 參考中的完整事件鉤子可以在這里找到

接下來,修改 search_fields 和 result_fields 以與數據集對齊。我們將通過圖書的名稱和作者進行搜索,并返回名稱、作者、image_url、url 和價格。

const config = {
...searchQuery: {search_fields: {name: {},author: {},},result_fields: {name: { raw: {} },author: { raw: {} },image_url: { raw: {} },url: { raw: {} },price: { raw: {} },release_date: { raw: {} }},},
};

最后,我們將添加幾個輔助函數來定義設備類型和用戶數據:

const getDeviceType = () => {const userAgent = navigator.userAgent.toLowerCase();if (/tablet|ipad|playbook|silk/.test(userAgent)) {return 'tablet';}if (/mobile|iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/.test(userAgent)) {return 'mobile';}return 'desktop';
};const getLocationData = async () => {const response = await fetch('https://ipapi.co/json/');const data = await response.json();return {ip: data.ip,city: data.city,region: data.region,country: data.country_name,latitude: data.latitude,longitude: data.longitude};
};

其余的 config 對象可以保持不變。

我們整理了一個倉庫,你可以在這里找到。它包含了更完整的項目版本。可以通過以下命令克隆:

git clone https://github.com/llermaly/search-ui-ubi.git

如果你使用 GitHub 倉庫,需要為服務器提供以下環境變量:

ELASTICSEARCH_HOST=your_elasticsearch_url
ELASTICSEARCH_API_KEY=your_api_key
ELASTICSEARCH_INDEX=books

運行應用程序

現在你可以啟動服務器:

cd server
npm install && node index.js

如果遇到與該庫相關的錯誤,可能需要單獨安裝 CORS:

npm install cors

在另一個終端中:

cd client
npm install && npm start

然后在瀏覽器中訪問 http://localhost:3000。

最終效果將如下所示:

在 Elasticsearch 端,我們可以為 ubi_events 索引創建一個(相當簡單的)映射,以便將用戶位置作為位置處理:

PUT ubi_events
{"mappings": {"properties": {"event_attributes.object.user.location": {"type": "geo_point"}}}
}

每次搜索時,會生成一個 ubi_queries 事件,而在點擊時,會生成一個類型為 click 的 ubi_events。

這就是一個 ubi_queries 事件的樣子:

{"_index": "ubi_queries","_id": "aCXqW5gB87F1AivbVvHI","_score": null,"_ignored": ["query.keyword"],"_source": {"query_response_id": "a8aca3d9-1cbc-4800-8853-fd1889172b9b","user_query": "snow","query_id": "d198c517-7d3b-49dd-be11-f573728d578e","query_response_object_ids": ["0","6"],"query": """{"from":0,"size":20,"query":{"multi_match":{"query":"snow","fields":["author^1.0","name^1.0"]}},"_source":{"includes":["name","author","image_url","url","price","release_date"],"excludes":[]},"sort":[{"_score":{"order":"desc"}}],"ext":{"query_id":"d198c517-7d3b-49dd-be11-f573728d578e","user_query":"snow","client_id":"8a5de3a1-7a1b-47ed-b64f-5be0537829be","object_id_field":null,"query_attributes":{}}}""","query_attributes": {},"client_id": "8a5de3a1-7a1b-47ed-b64f-5be0537829be","timestamp": 1753888741063},"sort": [1753888741063]}

這是一個示例 ubi_events 文檔:

{"_index": "ubi_events","_id": "fiDqW5gBftHcGY9PXtao","_score": null,"_source": {"application": "search-ui","action_name": "click","query_id": "3850340e-0e72-4f20-a06e-27a52d983b39","client_id": "8a5de3a1-7a1b-47ed-b64f-5be0537829be","timestamp": "2025-07-30T15:19:02.659Z","message_type": "CLICK_THROUGH","message": "Clicked Snow Crash","user_query": "snow","event_attributes": {"object": {"device": "desktop","object_id": "vrFBK5gBZjU2lCOmiNSX","description": "Snow Crash(1992-06-01) by Neal Stephenson","position": {"ordinal": 0,"page_depth": 1},"user": {"ip": "2800:bf0:108:18:d5ca:fa84:416f:99e0","city": "Quito","region": "Pichincha","country": "Ecuador","location": {"lat": -0.2309,"lon": -78.5211}}}}},"sort": [1753888742659]}

從這里,我們已經可以看到有用的信息,比如與特定查詢相關的操作。

結論

將 search-ui 與 UBI 擴展集成是一個可以收集用戶行為的寶貴見解的過程,并可以通過其他元數據擴展,例如用戶位置和設備類型。這些信息會自動索引到兩個獨立的索引中,分別用于查詢和操作,并可以通過唯一 ID 關聯。這些信息使開發者能夠更好地理解用戶如何使用應用,并優先處理可能影響用戶體驗的問題。

原文:Using UBI in Elasticsearch: Creating an app with UBI and search-ui - Elasticsearch Labs

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/97752.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/97752.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/97752.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

SpringBoot3中使用Caffeine緩存組件

SpringBoot3已經把EhCache從框架中刪除了&#xff0c;SpringBoot3默認的緩存組件為Caffeine&#xff0c;那么我們在SpringBoot3中如何去使用它了&#xff1f; 1.添加依賴 <dependency><groupId>com.github.ben-manes.caffeine</groupId><artifactId>ca…

正則表達式與grep文本過濾詳解

文章目錄前言一、正則表達式概述1.1 定義1.2 主要用途1.3 Linux 中的正則表達式分類1.3.1 基礎正則表達式&#xff08;BRE&#xff09;1.3.2 擴展正則表達式&#xff08;ERE&#xff09;二、正則表達式的基本組成2.1 普通字符2.2 元字符2.2.1 基本元字符2.2.2 重復次數相關2.2.…

Dify 集成 Milvus 配置指南

&#x1f9e9; Dify 集成 Milvus 配置指南 &#x1f527; 詳細配置步驟 1. 環境準備與克隆倉庫 首先確保你的系統已安裝 Git、Docker 和 Docker Compose。然后克隆 Dify 的代碼倉庫&#xff1a; git clone https://github.com/langgenius/dify.git cd dify/docker2. 配置環境變…

為不平,不止于此

口碑可以成就一個人&#xff0c;也可以毀掉一個人&#xff0c; 所以我們選擇用實力去創造兩種無聲的口碑。 要么讓期待的你張口而呼&#xff0c; 要么讓挑剔的你啞口無言。瑪哈特科技創始人 #為不平&#xff0c;不止于此#

0902 C++類的匿名對象

Part 1.梳理思維導圖一.匿名對象1.概念沒有對象名的類對象2.格式類名();3.作用1.給有名對象初始化2.給對象數組初始化3.作為函數的參數傳遞給形參4.例子#include <iostream>using namespace std;class Dog {friend void Dogfriend(Dog &b); private:string name;int …

在 PySpark 中解鎖窗口函數的力量,實現高級數據轉換

本篇文章Mastering PySpark Window Functions: A Practical Guide to Time-Based Analytics適合數據分析和工程師入門了解PySpark的窗口函數。文章的亮點在于詳細介紹了窗口函數的基本概念及其在銷售數據分析中的實際應用&#xff0c;幫助讀者理解如何進行復雜的數據計算而無需…

從理念到實踐:三層解耦架構與“無系統”論

在上一篇中&#xff0c;我們揭示了“五層雙閉環”治理模型如何像骨骼一樣&#xff0c;為數字化轉型提供支撐和定型。但再宏偉的藍圖也需要堅實的施工來實現。今天&#xff0c;我們將深入最具體的實施層面&#xff0c;將“業務重塑”和“以人為本”的理念&#xff0c;轉化為可落…

詳細介紹Linux 內存管理struct page數據結構中的_count和_mapcount有什么區別?

在Linux內核的struct page中&#xff0c;_count&#xff08;或_refcount&#xff09;和_mapcount是兩個關鍵的引用計數成員&#xff0c;它們各自承擔不同的職責。以下是深度解析和代碼案例&#xff1a;1. _count vs _mapcount 區別詳解_count&#xff08;或_refcount&#xff0…

面陣 vs 線陣相機:怎么選不踩坑?選型公式直接套用

面陣vs線陣相機&#xff1a;怎么選不踩坑&#xff1f;選型公式直接套用&#x1f3af;面陣vs線陣相機怎么選不踩坑&#xff1f;&#x1f3af;一、面陣相機&#xff1a;工業檢測的“萬能選手”&#xff0c;拍全圖靠它&#x1f3af;二、線陣相機&#xff1a;大視野/高精度的“專屬…

Spring Security 如何使用@PreAuthorize注解

&#x1f9f1; 第一步&#xff1a;環境準備? 1. 創建數據庫&#xff08;MySQL&#xff09;-- 創建數據庫&#xff0c;使用 utf8mb4 字符集支持 emoji 和多語言 CREATE DATABASE security_demo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;-- 使用該數據庫 USE security…

JVM中產生OOM(內存溢出)的8種典型情況及解決方案

Java中的OutOfMemoryError&#xff08;OOM&#xff09;是當JVM內存不足時拋出的錯誤。本文將全面剖析JVM中產生OOM的各種情況&#xff0c;包括堆內存溢出、方法區溢出、棧溢出等&#xff0c;并提供詳細的診斷方法和解決方案。 一、OOM基礎概念 1.1 OOM錯誤類型 Java中的OOM是…

【IEEE出版、EI檢索、往屆會后3個月檢索】第四屆信號處理、計算機網絡與通信國際學術會議(SPCNC 2025)

第四屆信號處理、計算機網絡與通信國際學術會議&#xff08;SPCNC 2025&#xff09;將于2025年12月5-7日于中國武漢召開&#xff08;線上同步&#xff09;。為本次會議旨在齊聚海內外信號處理、計算機網絡與通信等計算機領域的專家學者&#xff0c;為相關領域研究和從業人員提供…

Spring boot注解介紹

1. Spring 核心注解Spring Boot 是基于 Spring 框架的&#xff0c;所以核心注解依然適用。? 常見核心注解Component表示一個通用組件&#xff0c;Spring 會自動掃描并注入到容器中。Component public class MyComponent {public void sayHello() {System.out.println("He…

撤銷回退 情況?:已經 add ,但沒有 commit

撤銷回退 情況?&#xff1a;已經 add &#xff0c;但沒有 commit add 后還是保存到了暫存區呢&#xff1f;怎么撤銷呢&#xff1f; 1 # 向ReadMe中新增??代碼 2 hyb139-159-150-152:~/gitcode$ vim ReadMe 3 hyb139-159-150-152:~/gitcode$ cat ReadMe 4 hello bit 5 hell…

【Linux筆記】命令行與vim基礎

一、Linux命令行基礎 1. 基本語法命令空格參數&#xff08;可寫可不寫&#xff09;空格文件&#xff0c;文件夾&#xff08;可寫可不寫&#xff09;ls列出文件夾中的內容/opt 根目錄下的opt文件夾ls-a all顯示出所有文件以及隱藏文件/optls-a如果不寫則輸出一個點&#xff0c;當…

Redis 的整數集合:像分類收納盒一樣的整數專屬存儲

目錄 一、先懂定位&#xff1a;為什么需要整數集合&#xff1f;&#xff08;銜接哈希表&#xff09; 二、整數集合的結構&#xff1a;像 “貼了規格標簽的收納盒” 1. encoding&#xff1a;收納盒的 “規格標簽”&#xff08;核心&#xff1a;決定格子大小&#xff09; 2. …

Linux 進程狀態 — 僵尸進程

&#x1f381;個人主頁&#xff1a;工藤新一 &#x1f50d;系列專欄&#xff1a;C面向對象&#xff08;類和對象篇&#xff09; &#x1f31f;心中的天空之城&#xff0c;終會照亮我前方的路 &#x1f389;歡迎大家點贊&#x1f44d;評論&#x1f4dd;收藏?文章 文章目錄進…

React 中 key 的作用

React 中 key 的作用是什么&#xff1f; Date: August 31, 2025 Area: 原理key 概念 在 React 中&#xff0c;key 用于識別哪些元素是變化、添加或刪除的。 在列表渲染中&#xff0c;key 尤其重要&#xff0c;因為它能提高渲染性能和確保組件狀態的一致性。key 的作用 1&#x…

wpf之附加屬性

前言 附加屬性是 WPF 中一個非常強大和獨特的概念。簡單來說&#xff0c;它允許一個對象為另一個在其本身類定義中未定義的屬性賦值。 1、定義附加屬性 定義一個Watermark的附加屬性&#xff0c;該屬性的作用是將TextBox的附加屬性改變時&#xff0c;TextBox的字體顏色改成灰…

深入淺出 RabbitMQ-消息可靠性投遞

大家好&#xff0c;我是工藤學編程 &#x1f989;一個正在努力學習的小博主&#xff0c;期待你的關注實戰代碼系列最新文章&#x1f609;C實現圖書管理系統&#xff08;Qt C GUI界面版&#xff09;SpringBoot實戰系列&#x1f437;【SpringBoot實戰系列】SpringBoot3.X 整合 Mi…