Skip to main content

คู่มือการเชื่อมต่อ 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 มาตรฐาน​

ProtocolURL
HTTPhttp://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เวอร์ชันที่แนะนำ
PHPelasticsearch/elasticsearch^9.0
Pythonelasticsearch>=9.0,<10.0
Javaco.elastic.clients:elasticsearch-java9.3.x
Node.js@elastic/elasticsearch^9.0
C# (.NET 5+)Elastic.Clients.Elasticsearch9.x
C# (.NET FW 4.5)HttpClient + Newtonsoft.Json—
note

หากยังใช้ 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.5HttpClient + Newtonsoft.JsonLibrary ใหม่ไม่รองรับ
.NET Framework 4.6.1–4.8.1Elastic.Clients.Elasticsearch 9.xผ่าน .NET Standard 2.0
.NET 5Elastic.Clients.Elasticsearch 9.xEOL แล้ว แนะนำอัปเกรด
.NET 6 (LTS)Elastic.Clients.Elasticsearch 9.xTop-level statements
.NET 7Elastic.Clients.Elasticsearch 9.xRequired members
.NET 8 (LTS)Elastic.Clients.Elasticsearch 9.xPrimary constructors
.NET 9Elastic.Clients.Elasticsearch 9.xparams collections
.NET 10 (LTS)Elastic.Clients.Elasticsearch 9.xfield 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 สถาปัตยกรรมที่แนะนำ​

ห้ามเชื่อมต่อ Elasticsearch โดยตรงจาก Browser

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)
  • ใช้ _source filtering เพื่อดึงเฉพาะ 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: 8 header
  • 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 ไม่เจอผล (text vs keyword ให้พฤติกรรมต่างกัน)
  • ใช้ _analyze API เพื่อดูว่า text ถูก tokenize อย่างไร
  • .NET: เปิด OnRequestCompleted event ใน ElasticsearchClientSettings เพื่อ log HTTP

.NET LTS Roadmap​

เวอร์ชันประเภทสิ้นสุด Support
.NET 6LTSพฤศจิกายน 2024
.NET 7STSพฤษภาคม 2024
.NET 8LTSพฤศจิกายน 2026
.NET 9STSพฤษภาคม 2026
.NET 10LTSพฤศจิกายน 2028

สำหรับโปรเจกต์ใหม่ แนะนำใช้ .NET 10 (LTS) หรือ .NET 8 หากต้องการความเสถียรที่ผ่านการใช้งานมานานกว่า