คู่มือการเชื่อมต่อ Elasticsearch 9.3.x
ฉบับปรับปรุง 04/2026
1. ภาพรวม
1.1 เวอร์ชันที่อ้างอิง
คู่มือนี้อ้างอิง Elasticsearch 9.3.x ซึ่งเป็นเวอร์ชันล่าสุด ณ ปี 2026 REST API ยังคงรูปแบบเดิมกับ 8.x เกือบทั้งหมด แต่ client library ต้องตรง major version กับ server
1.2 Endpoint มาตรฐาน
| Protocol | URL |
|---|---|
| HTTP | http://eventlog.in.psu.ac.th:9200 |
| HTTPS (แนะนำ) | https://eventlog.in.psu.ac.th:9200 |
1.3 Authentication
Elasticsearch 9.x เปิด security เป็นค่าเริ่มต้น ต้องใช้วิธีใดวิธีหนึ่งต่อไปนี้:
- Username/Password — สำหรับทดสอบ/ใช้งานภายใน
- API Key — แนะนำสำหรับ application
- Bearer Token — สำหรับ service account
- CA Certificate / Fingerprint — กรณี HTTPS แบบ self-signed
1.4 Client Version Compatibility
| ภาษา | Package / Artifact | เวอร์ชันที่แนะนำ |
|---|---|---|
| PHP | elasticsearch/elasticsearch | ^9.0 |
| Python | elasticsearch | >=9.0,<10.0 |
| Java | co.elastic.clients:elasticsearch-java | 9.3.x |
| Node.js | @elastic/elasticsearch | ^9.0 |
| C# (.NET 5+) | Elastic.Clients.Elasticsearch | 9.x |
| C# (.NET FW 4.5) | HttpClient + Newtonsoft.Json | — |
หากยังใช้ client 8.x และต้องการต่อกับ server 9.x ชั่วคราว สามารถใช้ header Accept-Version: 8 ได้ระหว่าง migrate แต่ไม่แนะนำสำหรับ production ระยะยาว
1.5 Data Structure ตัวอย่าง
คู่มือนี้ใช้ index ชื่อ products ที่มี field ดังนี้:
{
"name": "ชื่อสินค้า",
"price": 1000,
"category": "หมวดหมู่",
"stock": 50
}
1.6 CA Certificate (http_ca.crt)
-----BEGIN CERTIFICATE-----
MIIEHzCCA6agAwIBAgIRAPzxBwZjcuDmtE6oupUR1aIwCgYIKoZIzj0EAwMwSzEL
MAkGA1UEBhMCQVQxEDAOBgNVBAoTB1plcm9TU0wxKjAoBgNVBAMTIVplcm9TU0wg
RUNDIERvbWFpbiBTZWN1cmUgU2l0ZSBDQTAeFw0yNjAxMjUwMDAwMDBaFw0yNjA0
MjUyMzU5NTlaMBkxFzAVBgNVBAMMDiouaW4ucHN1LmFjLnRoMHYwEAYHKoZIzj0C
AQYFK4EEACIDYgAES1YGb0Ts1xeS9rE0P8sdWOKNVqA0dXAZpbfA7rZsNTbAY0HB
mBfwy4uN7Ji8Ls0lJSN/+d0P/q295Ugu4Zrn0U/T7xn327EvAi+shsNnOGKK+B4n
mdVosS5+giIsxb06o4ICfjCCAnowHwYDVR0jBBgwFoAUD2vmS845R672fpAeefAw
kZLIX6MwHQYDVR0OBBYEFDT0R17jbf3dJqTp2WP4AVPHmh9UMA4GA1UdDwEB/wQE
AwIHgDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsGAQUFBwMBMEkGA1UdIARC
MEAwNAYLKwYBBAGyMQECAk4wJTAjBggrBgEFBQcCARYXaHR0cHM6Ly9zZWN0aWdv
LmNvbS9DUFMwCAYGZ4EMAQIBMIGIBggrBgEFBQcBAQR8MHowSwYIKwYBBQUHMAKG
P2h0dHA6Ly96ZXJvc3NsLmNydC5zZWN0aWdvLmNvbS9aZXJvU1NMRUNDRG9tYWlu
U2VjdXJlU2l0ZUNBLmNydDArBggrBgEFBQcwAYYfaHR0cDovL3plcm9zc2wub2Nz
cC5zZWN0aWdvLmNvbTCCAQQGCisGAQQB1nkCBAIEgfUEgfIA8AB3AA5XlLzzrqk+
MxssmQez95Dfm8I9cTIl3SGpJaxhxU4hAAABm/Lsc3gAAAQDAEgwRgIhAN9gSHoM
RPGpVxt+ITFj3IV6adwWJFc8u4+3sDSUS3ZXAiEA2kiUT2/+HToONJrNrs/QAxMy
3tgj5C8Fk5mvLC80LF4AdQAWgy2r8KklDw/wOqVF/8i/yCPQh0v2BCkn+OcfMxP1
+gAAAZvy7HNxAAAEAwBGMEQCIEwMMKratjUHC3TmpS4i4ozjN4dIIYw/hfqesR+R
nNlQAiBTodypCCmi5jkIz+oaWSkq+zERaEa/IcLAXljiRicwmDAnBgNVHREEIDAe
gg4qLmluLnBzdS5hYy50aIIMaW4ucHN1LmFjLnRoMAoGCCqGSM49BAMDA2cAMGQC
MEe5C/QqYaOoV3/Mm8Rw5QCyfhMobL4LIZ4smsdXPgUeAkJqkgvav5qasEhR1Txm
4gIwf2jeeYOAlPXtzk2PKqPsPEI/m7c5cf6zjWKyRrxOmsKmhFVnq9xrN+oDuhnF
prnu
-----END CERTIFICATE-----
2. cURL
เหมาะสำหรับทดสอบเบื้องต้นและใช้ใน shell script
ทดสอบการเชื่อมต่อ
curl -X GET "https://eventlog.in.psu.ac.th:9200/" \
-u elastic:your_password \
--cacert /path/to/http_ca.crt
เชื่อมต่อด้วย API Key
curl -X GET "https://eventlog.in.psu.ac.th:9200/" \
-H "Authorization: ApiKey YOUR_BASE64_API_KEY" \
--cacert /path/to/http_ca.crt
สร้าง Index พร้อม Mapping
curl -X PUT "https://eventlog.in.psu.ac.th:9200/products" \
-u elastic:your_password \
--cacert /path/to/http_ca.crt \
-H "Content-Type: application/json" \
-d '{
"mappings": {
"properties": {
"name": { "type": "text" },
"price": { "type": "float" },
"category": { "type": "keyword" },
"stock": { "type": "integer" }
}
}
}'
เพิ่ม Document
curl -X POST "https://eventlog.in.psu.ac.th:9200/products/_doc/1" \
-u elastic:your_password \
--cacert /path/to/http_ca.crt \
-H "Content-Type: application/json" \
-d '{"name":"iPhone 15","price":35000,"category":"mobile","stock":20}'
ค้นหา Document
curl -X GET "https://eventlog.in.psu.ac.th:9200/products/_search" \
-u elastic:your_password \
--cacert /path/to/http_ca.crt \
-H "Content-Type: application/json" \
-d '{"query":{"match":{"name":"iPhone"}}}'
แก้ไข Document
curl -X POST "https://eventlog.in.psu.ac.th:9200/products/_update/1" \
-u elastic:your_password \
--cacert /path/to/http_ca.crt \
-H "Content-Type: application/json" \
-d '{"doc":{"price":32000,"stock":15}}'
ลบ Document
curl -X DELETE "https://eventlog.in.psu.ac.th:9200/products/_doc/1" \
-u elastic:your_password \
--cacert /path/to/http_ca.crt
3. PHP
ต้องการ PHP 8.1+
ติดตั้ง
composer require elasticsearch/elasticsearch:^9.0
สร้างการเชื่อมต่อ
<?php
require 'vendor/autoload.php';
use Elastic\Elasticsearch\ClientBuilder;
// Basic Auth
$client = ClientBuilder::create()
->setHosts(['https://eventlog.in.psu.ac.th:9200'])
->setBasicAuthentication('elastic', 'your_password')
->setCABundle('/path/to/http_ca.crt')
->build();
// หรือ API Key
// $client = ClientBuilder::create()
// ->setHosts(['https://eventlog.in.psu.ac.th:9200'])
// ->setApiKey('YOUR_API_KEY_BASE64')
// ->setCABundle('/path/to/http_ca.crt')
// ->build();
$info = $client->info();
print_r($info->asArray());
CRUD
// สร้าง Index
$client->indices()->create([
'index' => 'products',
'body' => [
'mappings' => [
'properties' => [
'name' => ['type' => 'text'],
'price' => ['type' => 'float'],
'category' => ['type' => 'keyword'],
'stock' => ['type' => 'integer'],
],
],
],
]);
// เพิ่ม Document
$client->index([
'index' => 'products',
'id' => '1',
'body' => ['name' => 'iPhone 15', 'price' => 35000, 'category' => 'mobile', 'stock' => 20],
]);
// ค้นหา
$response = $client->search([
'index' => 'products',
'body' => ['query' => ['match' => ['name' => 'iPhone']]],
]);
foreach ($response['hits']['hits'] as $hit) {
echo $hit['_source']['name'] . ' - ' . $hit['_source']['price'] . "\n";
}
// แก้ไข
$client->update([
'index' => 'products',
'id' => '1',
'body' => ['doc' => ['price' => 32000, 'stock' => 15]],
]);
// ลบ
$client->delete(['index' => 'products', 'id' => '1']);
Error Handling
use Elastic\Elasticsearch\Exception\ClientResponseException;
use Elastic\Elasticsearch\Exception\ServerResponseException;
try {
$response = $client->get(['index' => 'products', 'id' => '999']);
} catch (ClientResponseException $e) {
echo 'Client error: ' . $e->getMessage();
} catch (ServerResponseException $e) {
echo 'Server error: ' . $e->getMessage();
}
4. Python
ต้องการ Python 3.9+
ติดตั้ง
pip install "elasticsearch>=9.0,<10.0"
สร้างการเชื่อมต่อ
from elasticsearch import Elasticsearch
# วิธีที่ 1: Basic Auth + CA cert
es = Elasticsearch(
"https://eventlog.in.psu.ac.th:9200",
basic_auth=("elastic", "your_password"),
ca_certs="/path/to/http_ca.crt",
)
# วิธีที่ 2: API Key (แนะนำสำหรับ production)
es = Elasticsearch(
"https://eventlog.in.psu.ac.th:9200",
api_key="your_api_key_here",
ca_certs="/path/to/http_ca.crt",
)
print(es.info())
CRUD
# สร้าง Index
es.indices.create(index="products", body={
"mappings": {
"properties": {
"name": {"type": "text"},
"price": {"type": "float"},
"category": {"type": "keyword"},
"stock": {"type": "integer"},
}
}
})
# เพิ่ม Document
es.index(index="products", id="1", document={
"name": "iPhone 15", "price": 35000, "category": "mobile", "stock": 20,
})
# ค้นหา
response = es.search(index="products", body={"query": {"match": {"name": "iPhone"}}})
for hit in response["hits"]["hits"]:
print(hit["_source"]["name"], "-", hit["_source"]["price"])
# แก้ไข
es.update(index="products", id="1", body={"doc": {"price": 32000, "stock": 15}})
# ลบ
es.delete(index="products", id="1")
Bulk Operation
from elasticsearch.helpers import bulk
actions = [
{"_index": "products", "_id": i, "_source": {"name": f"Product {i}", "price": 100 * i, "category": "general", "stock": 10}}
for i in range(1, 101)
]
bulk(es, actions)
Error Handling
from elasticsearch import NotFoundError, ConnectionError
try:
es.get(index="products", id="999")
except NotFoundError:
print("ไม่พบ document")
except ConnectionError as e:
print(f"เชื่อมต่อไม่ได้: {e}")
5. Java
ต้องการ Java 17+
ติดตั้ง Dependency
<!-- Maven pom.xml -->
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>9.3.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.18.0</version>
</dependency>
// Gradle build.gradle
implementation 'co.elastic.clients:elasticsearch-java:9.3.0'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.0'
สร้างการเชื่อมต่อ
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.elasticsearch.client.RestClient;
public class ESConnection {
public static ElasticsearchClient createClient() {
var credsProv = new BasicCredentialsProvider();
credsProv.setCredentials(AuthScope.ANY,
new UsernamePasswordCredentials("elastic", "your_password"));
var restClient = RestClient
.builder(new HttpHost("eventlog.in.psu.ac.th", 9200, "https"))
.setHttpClientConfigCallback(hc -> hc.setDefaultCredentialsProvider(credsProv))
.build();
return new ElasticsearchClient(
new RestClientTransport(restClient, new JacksonJsonpMapper())
);
}
}
Document Model
public class Product {
private String name;
private Double price;
private String category;
private Integer stock;
public Product() {}
public Product(String name, Double price, String category, Integer stock) {
this.name = name; this.price = price;
this.category = category; this.stock = stock;
}
// getters / setters omitted for brevity
}
CRUD
// สร้าง Index
client.indices().create(c -> c
.index("products")
.mappings(m -> m
.properties("name", p -> p.text(t -> t))
.properties("price", p -> p.float_(f -> f))
.properties("category", p -> p.keyword(k -> k))
.properties("stock", p -> p.integer(i -> i))
)
);
// เพิ่ม Document
var product = new Product("iPhone 15", 35000.0, "mobile", 20);
var resp = client.index(i -> i.index("products").id("1").document(product));
System.out.println(resp.result());
// ค้นหา
var searchResp = client.search(s -> s
.index("products")
.query(q -> q.match(t -> t.field("name").query("iPhone"))),
Product.class
);
for (var hit : searchResp.hits().hits()) {
System.out.println(hit.source().getName() + " - " + hit.source().getPrice());
}
// แก้ไข
client.update(u -> u
.index("products").id("1")
.doc(Map.of("price", 32000, "stock", 15)),
Product.class
);
// ลบ
client.delete(d -> d.index("products").id("1"));
Error Handling
import co.elastic.clients.elasticsearch._types.ElasticsearchException;
try {
client.get(g -> g.index("products").id("999"), Product.class);
} catch (ElasticsearchException e) {
System.err.println("ES Error: " + e.error().reason());
} catch (IOException e) {
System.err.println("IO Error: " + e.getMessage());
}
6. C# — หลาย .NET เวอร์ชัน
6.1 Compatibility
| .NET Version | แนวทาง | หมายเหตุ |
|---|---|---|
| .NET Framework 4.5 | HttpClient + Newtonsoft.Json | Library ใหม่ไม่รองรับ |
| .NET Framework 4.6.1–4.8.1 | Elastic.Clients.Elasticsearch 9.x | ผ่าน .NET Standard 2.0 |
| .NET 5 | Elastic.Clients.Elasticsearch 9.x | EOL แล้ว แนะนำอัปเกรด |
| .NET 6 (LTS) | Elastic.Clients.Elasticsearch 9.x | Top-level statements |
| .NET 7 | Elastic.Clients.Elasticsearch 9.x | Required members |
| .NET 8 (LTS) | Elastic.Clients.Elasticsearch 9.x | Primary constructors |
| .NET 9 | Elastic.Clients.Elasticsearch 9.x | params collections |
| .NET 10 (LTS) | Elastic.Clients.Elasticsearch 9.x | field keyword (แนะนำ) |
6.2 .NET Framework 4.5 — HttpClient โดยตรง
Elastic.Clients.Elasticsearch ต้องการ .NET FW 4.6.1+ จึงต้องใช้ HttpClient แทน
# NuGet
Install-Package Newtonsoft.Json
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
public class EsClient
{
private readonly HttpClient _http;
private readonly string _baseUrl;
public EsClient(string baseUrl, string user, string pass)
{
// .NET FW 4.5 ไม่เปิด TLS 1.2 อัตโนมัติ — ต้องบังคับ
ServicePointManager.SecurityProtocol |= (SecurityProtocolType)3072;
_baseUrl = baseUrl.TrimEnd('/');
_http = new HttpClient();
var auth = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{user}:{pass}"));
_http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Basic", auth);
}
private StringContent Json(object obj) =>
new StringContent(JsonConvert.SerializeObject(obj), Encoding.UTF8, "application/json");
public Task<string> CreateIndexAsync(string index, object mapping) =>
_http.PutAsync($"{_baseUrl}/{index}", Json(mapping))
.ContinueWith(t => t.Result.Content.ReadAsStringAsync()).Unwrap();
public Task<string> IndexAsync(string index, string id, object doc) =>
_http.PostAsync($"{_baseUrl}/{index}/_doc/{id}", Json(doc))
.ContinueWith(t => t.Result.Content.ReadAsStringAsync()).Unwrap();
public Task<string> SearchAsync(string index, object query) =>
_http.PostAsync($"{_baseUrl}/{index}/_search", Json(query))
.ContinueWith(t => t.Result.Content.ReadAsStringAsync()).Unwrap();
public Task<string> UpdateAsync(string index, string id, object partial) =>
_http.PostAsync($"{_baseUrl}/{index}/_update/{id}", Json(new { doc = partial }))
.ContinueWith(t => t.Result.Content.ReadAsStringAsync()).Unwrap();
public Task<string> DeleteAsync(string index, string id) =>
_http.DeleteAsync($"{_baseUrl}/{index}/_doc/{id}")
.ContinueWith(t => t.Result.Content.ReadAsStringAsync()).Unwrap();
}
// .NET FW 4.5 ไม่รองรับ async Main — ต้อง wrap
static void Main() => RunAsync().GetAwaiter().GetResult();
static async Task RunAsync()
{
var es = new EsClient("https://eventlog.in.psu.ac.th:9200", "elastic", "your_password");
await es.CreateIndexAsync("products", new {
mappings = new { properties = new {
name = new { type = "text" },
price = new { type = "float" },
category = new { type = "keyword" },
stock = new { type = "integer" },
}}
});
await es.IndexAsync("products", "1",
new { name = "iPhone 15", price = 35000, category = "mobile", stock = 20 });
var result = await es.SearchAsync("products",
new { query = new { match = new { name = "iPhone" } } });
Console.WriteLine(result);
await es.UpdateAsync("products", "1", new { price = 32000, stock = 15 });
await es.DeleteAsync("products", "1");
}
6.3 .NET 5+ — Elastic.Clients.Elasticsearch 9.x
dotnet add package Elastic.Clients.Elasticsearch --version 9.*
สร้างการเชื่อมต่อ (.NET 6+ top-level style):
using Elastic.Clients.Elasticsearch;
using Elastic.Transport;
var settings = new ElasticsearchClientSettings(new Uri("https://eventlog.in.psu.ac.th:9200"))
.Authentication(new BasicAuthentication("elastic", "your_password"))
.CertificateFingerprint("YOUR_CERT_FINGERPRINT_HERE")
.DefaultIndex("products");
var client = new ElasticsearchClient(settings);
// หรือใช้ API Key
// .Authentication(new ApiKey("YOUR_BASE64_API_KEY"))
var info = await client.InfoAsync();
Console.WriteLine(info.Version.Number);
Model:
public class Product
{
public string Name { get; set; }
public double Price { get; set; }
public string Category { get; set; }
public int Stock { get; set; }
}
CRUD:
// สร้าง Index
await client.Indices.CreateAsync<Product>("products", c => c
.Mappings(m => m.Properties(p => p
.Text(t => t.Name)
.FloatNumber(f => f.Price)
.Keyword(k => k.Category)
.IntegerNumber(i => i.Stock)
))
);
// เพิ่ม Document
var product = new Product { Name = "iPhone 15", Price = 35000, Category = "mobile", Stock = 20 };
var indexResp = await client.IndexAsync(product, i => i.Index("products").Id("1"));
Console.WriteLine(indexResp.Result);
// ค้นหา
var searchResp = await client.SearchAsync<Product>(s => s
.Index("products")
.Query(q => q.Match(m => m.Field(f => f.Name).Query("iPhone")))
);
foreach (var hit in searchResp.Hits)
Console.WriteLine($"{hit.Source.Name} - {hit.Source.Price}");
// แก้ไข
await client.UpdateAsync<Product, object>("products", "1",
u => u.Doc(new { price = 32000, stock = 15 }));
// ลบ
await client.DeleteAsync("products", "1");
Error Handling:
var response = await client.GetAsync<Product>("products", "999");
if (!response.IsValidResponse)
{
var reason = response.ElasticsearchServerError?.Error.Reason
?? response.DebugInformation;
Console.WriteLine($"Error: {reason}");
}
else
{
Console.WriteLine($"Found: {response.Source.Name}");
}
6.4 Syntax ของแต่ละเวอร์ชัน .NET
API ยังคงเหมือนกัน แต่แต่ละเวอร์ชัน .NET มี C# features ใหม่ที่ช่วยย่อโค้ดได้
.NET 5 — C# 9 Records:
public record Product(string Name, double Price, string Category, int Stock);
.NET 6 — Top-level Statements & File-scoped Namespaces:
// Program.cs — ไม่ต้องมี Main() และ namespace block
using Elastic.Clients.Elasticsearch;
using Elastic.Transport;
var settings = new ElasticsearchClientSettings(new Uri("https://eventlog.in.psu.ac.th:9200"))
.Authentication(new BasicAuthentication("elastic", "your_password"))
.CertificateFingerprint("YOUR_FINGERPRINT")
.DefaultIndex("products");
var client = new ElasticsearchClient(settings);
var resp = await client.IndexAsync(
new Product("iPhone 15", 35000, "mobile", 20),
i => i.Index("products").Id("1"));
Console.WriteLine(resp.Result);
// file-scoped namespace (ลด indent 1 ระดับ)
public record Product(string Name, double Price, string Category, int Stock);
.NET 7 — Required Members & Raw String Literals:
public class Product
{
public required string Name { get; init; }
public required double Price { get; init; }
public required string Category { get; init; }
public required int Stock { get; init; }
}
// raw string literal สำหรับ JSON query
var rawQuery = """
{
"query": { "match": { "name": "iPhone" } }
}
""";
.NET 8 — Primary Constructors & Collection Expressions:
public class Product(string name, double price, string category, int stock)
{
public string Name { get; init; } = name;
public double Price { get; init; } = price;
public string Category { get; init; } = category;
public int Stock { get; init; } = stock;
}
// Service ที่ inject client ได้เลย ไม่ต้องเขียน constructor body
public class ProductService(ElasticsearchClient client)
{
public async Task<Product[]> SearchAsync(string keyword)
{
var resp = await client.SearchAsync<Product>(s => s
.Index("products")
.Query(q => q.Match(m => m.Field(f => f.Name).Query(keyword))));
return [.. resp.Documents]; // collection expression
}
}
.NET 9 — params Collections & System.Threading.Lock:
public class ProductService(ElasticsearchClient client)
{
// params ReadOnlySpan<T> — ไม่ allocate array
public async Task<long> SearchByCategoriesAsync(params ReadOnlySpan<string> categories)
{
var list = categories.ToArray();
var resp = await client.SearchAsync<Product>(s => s
.Index("products")
.Query(q => q.Terms(t => t
.Field(f => f.Category)
.Terms(new TermsQueryField(list.Select(FieldValue.String).ToArray())))));
return resp.Total;
}
}
// ใช้งาน — ไม่ต้อง new string[] { ... }
var count = await service.SearchByCategoriesAsync("mobile", "tablet", "laptop");
// System.Threading.Lock (ใหม่ใน .NET 9) — เร็วกว่า lock(object)
public class ConnectionPool
{
private readonly Lock _lock = new();
private ElasticsearchClient? _shared;
public ElasticsearchClient GetClient()
{
lock (_lock) { return _shared ??= CreateClient(); }
}
private ElasticsearchClient CreateClient() => /* ... */ null!;
}
.NET 10 — field Keyword:
public class Product
{
public required string Name { get; init; }
// ใช้ field keyword แทน private double _price;
public required double Price
{
get => field;
init => field = value < 0
? throw new ArgumentException("Price must be non-negative")
: value;
}
public required string Category { get; init; }
public required int Stock
{
get => field;
init => field = Math.Max(0, value); // ป้องกันค่าติดลบ
}
}
6.5 Dependency Injection ใน ASP.NET Core (.NET 6+)
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<ElasticsearchClient>(sp =>
{
var cfg = builder.Configuration;
var settings = new ElasticsearchClientSettings(new Uri(cfg["Elasticsearch:Uri"]!))
.Authentication(new BasicAuthentication(
cfg["Elasticsearch:Username"]!,
cfg["Elasticsearch:Password"]!))
.CertificateFingerprint(cfg["Elasticsearch:Fingerprint"]!)
.DefaultIndex("products");
return new ElasticsearchClient(settings);
});
builder.Services.AddScoped<ProductService>();
var app = builder.Build();
app.MapGet("/products/search",
async (string q, ProductService svc) => await svc.SearchAsync(q));
app.Run();
// appsettings.json
{
"Elasticsearch": {
"Uri": "https://eventlog.in.psu.ac.th:9200",
"Username": "elastic",
"Password": "your_password",
"Fingerprint": "YOUR_CERT_FINGERPRINT"
}
}
7. React
7.1 สถาปัตยกรรมที่แนะนำ
Credential (username/password, API Key) จะถูกเปิดเผยใน DevTools และถูกโจมตีผ่าน CORS / injection ได้ง่าย
ต้องใช้ Backend เป็น proxy เสมอ:
[React Frontend] ──HTTP──► [Node.js Backend] ──HTTPS──► [Elasticsearch]
7.2 ติดตั้ง Backend (Node.js + Express)
mkdir es-backend && cd es-backend
npm init -y
npm install express cors @elastic/elasticsearch@^9.0 dotenv
7.3 Elasticsearch Client (Backend)
// es-client.js
const { Client } = require('@elastic/elasticsearch');
const fs = require('fs');
const client = new Client({
node: 'https://eventlog.in.psu.ac.th:9200',
auth: {
username: process.env.ES_USERNAME || 'elastic',
password: process.env.ES_PASSWORD || 'your_password',
},
tls: {
ca: fs.readFileSync('/path/to/http_ca.crt'),
rejectUnauthorized: true,
},
});
module.exports = client;
7.4 Express API Routes
// server.js
const express = require('express');
const cors = require('cors');
const client = require('./es-client');
const app = express();
app.use(cors());
app.use(express.json());
// ค้นหา
app.get('/api/products/search', async (req, res) => {
try {
const { q } = req.query;
const result = await client.search({
index: 'products',
query: q ? { match: { name: q } } : { match_all: {} },
});
res.json({
total: result.hits.total.value,
products: result.hits.hits.map(h => ({ id: h._id, ...h._source })),
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// เพิ่ม
app.post('/api/products', async (req, res) => {
try {
const result = await client.index({ index: 'products', document: req.body });
res.status(201).json({ id: result._id, ...req.body });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// แก้ไข
app.put('/api/products/:id', async (req, res) => {
try {
await client.update({ index: 'products', id: req.params.id, doc: req.body });
res.json({ id: req.params.id, ...req.body });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ลบ
app.delete('/api/products/:id', async (req, res) => {
try {
await client.delete({ index: 'products', id: req.params.id });
res.json({ deleted: true });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// Error middleware
app.use((err, req, res, next) => {
console.error('[ES Error]', err);
res.status(err.meta?.statusCode || 500).json({
error: err.meta?.body?.error ?? err.message,
});
});
app.listen(3001, () => console.log('Server running on port 3001'));
7.5 ติดตั้ง React App
npm create vite@latest my-app -- --template react
cd my-app && npm install axios
7.6 Custom Hooks
// src/hooks/useDebounce.js
import { useState, useEffect } from 'react';
export function useDebounce(value, delay = 300) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const t = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(t);
}, [value, delay]);
return debounced;
}
// src/hooks/useSearchProducts.js
import { useState, useEffect } from 'react';
import axios from 'axios';
const API_BASE = 'http://localhost:3001/api';
export function useSearchProducts(query) {
const [data, setData] = useState({ products: [], total: 0 });
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
setError(null);
axios
.get(`${API_BASE}/products/search`, { params: { q: query } })
.then(res => { if (!cancelled) setData(res.data); })
.catch(err => { if (!cancelled) setError(err); })
.finally(() => { if (!cancelled) setLoading(false); });
return () => { cancelled = true; };
}, [query]);
return { data, loading, error };
}
7.7 Components
// src/components/SearchProducts.jsx
import { useState } from 'react';
import { useDebounce } from '../hooks/useDebounce';
import { useSearchProducts } from '../hooks/useSearchProducts';
export default function SearchProducts() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
const { data, loading, error } = useSearchProducts(debouncedQuery);
return (
<div style={{ maxWidth: 600, margin: '20px auto' }}>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="ค้นหาสินค้า..."
style={{ width: '100%', padding: 10, fontSize: 16 }}
/>
{loading && <p>กำลังค้นหา...</p>}
{error && <p style={{ color: 'red' }}>Error: {error.message}</p>}
<p>พบ {data.total} รายการ</p>
<ul>
{data.products.map(p => (
<li key={p.id}>
<strong>{p.name}</strong> — ฿{p.price?.toLocaleString()}
<span style={{ color: '#888' }}> ({p.category})</span>
<span> คงเหลือ {p.stock} ชิ้น</span>
</li>
))}
</ul>
</div>
);
}
// src/components/AddProduct.jsx
import { useState } from 'react';
import axios from 'axios';
export default function AddProduct({ onAdded }) {
const [form, setForm] = useState({ name: '', price: '', category: '', stock: '' });
const [submitting, setSubmit] = useState(false);
const set = field => e => setForm(f => ({ ...f, [field]: e.target.value }));
const handleSubmit = async e => {
e.preventDefault();
setSubmit(true);
try {
await axios.post('http://localhost:3001/api/products', {
name: form.name, price: Number(form.price),
category: form.category, stock: Number(form.stock),
});
setForm({ name: '', price: '', category: '', stock: '' });
onAdded?.();
} catch (err) {
alert('Error: ' + (err.response?.data?.error ?? err.message));
} finally {
setSubmit(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input value={form.name} onChange={set('name')} placeholder="ชื่อสินค้า" required />
<input value={form.price} onChange={set('price')} placeholder="ราคา" required type="number" />
<input value={form.category} onChange={set('category')} placeholder="หมวดหมู่" required />
<input value={form.stock} onChange={set('stock')} placeholder="สต็อก" required type="number" />
<button type="submit" disabled={submitting}>
{submitting ? 'กำลังบันทึก...' : 'เพิ่มสินค้า'}
</button>
</form>
);
}
8. เคล็ดลับและข้อควรระวัง
Connection Pool
- Client library ทุกภาษา (Python / PHP / Java / C# / Node.js) จัดการ connection pooling อัตโนมัติ — สร้าง client เป็น singleton แล้วใช้ซ้ำ
- อย่าสร้าง client ใหม่ทุก request
- ใน ASP.NET Core:
AddSingleton<ElasticsearchClient>ใน DI container
Security ใน Production
- ใช้ HTTPS เสมอ — ห้ามตั้ง
verify_certs=FalseหรือrejectUnauthorized=false - ใช้ API Key แทน username/password สำหรับ application
- กำหนด role ตามหลัก least privilege
- เก็บ credential ใน environment variable / secret manager ไม่เก็บใน source code
- React: ห้ามเชื่อมต่อ ES โดยตรงจาก browser — ต้องผ่าน backend เสมอ
- .NET FW 4.5: บังคับ TLS 1.2 ผ่าน
ServicePointManagerก่อนใช้งาน
Performance
- ใช้ Bulk API เมื่อ index document จำนวนมาก (เร็วกว่าทีละตัวหลายเท่า)
- ปรับ
refresh_intervalตามโหลด (ค่าเริ่มต้น 1s — write เยอะอาจตั้งเป็น 30s) - ใช้
_sourcefiltering เพื่อดึงเฉพาะ field ที่ต้องการ - ใช้ pagination (
from/sizeหรือsearch_after) เมื่อผลลัพธ์มีจำนวนมาก - React: ใช้ debounce กับ search input เพื่อลด request
Version Compatibility
- Client version ควรตรงกับ server (อย่างน้อย major version เดียวกัน)
- Elasticsearch 8.x → 9.x: API compatibility สูง แต่ควรอัปเดต client ด้วย
- Migrate ชั่วคราวได้ด้วย
Accept-Version: 8header - Java: ย้ายจาก
RestHighLevelClient(deprecated) มาใช้ Elasticsearch Java Client ใหม่ - C#: ย้ายจาก NEST มาใช้
Elastic.Clients.Elasticsearch(ตั้งแต่ 8.x)
Debugging
- เปิด log request/response เพื่อดู query จริงที่ส่งไป
- ทดสอบ query ใน Kibana Dev Tools ก่อนแปลงเป็นโค้ด
- ตรวจ index mapping เสมอเมื่อ search ไม่เจอผล (
textvskeywordให้พฤติกรรมต่างกัน) - ใช้
_analyzeAPI เพื่อดูว่า text ถูก tokenize อย่างไร - .NET: เปิด
OnRequestCompletedevent ในElasticsearchClientSettingsเพื่อ log HTTP
.NET LTS Roadmap
| เวอร์ชัน | ประเภท | สิ้นสุด Support |
|---|---|---|
| .NET 6 | LTS | พฤศจิกายน 2024 |
| .NET 7 | STS | พฤษภาคม 2024 |
| .NET 8 | LTS | พฤศจิกายน 2026 |
| .NET 9 | STS | พฤษภาคม 2026 |
| .NET 10 | LTS | พฤศจิกายน 2028 |
สำหรับโปรเจกต์ใหม่ แนะนำใช้ .NET 10 (LTS) หรือ .NET 8 หากต้องการความเสถียรที่ผ่านการใช้งานมานานกว่า