Elasticsearch Java Client用戶指南

翻譯自官方文檔英文版,有刪減。

這里使用的Java客戶端版本是5.1.2,Elasticsearch的版本號也要是5.1.2,否則一些功能可能不支持。

之前介紹過Spring Data Elasticsearch,那里也是使用了本文介紹的官方客戶端,只不過Spring Data Elasticsearch是一個社區項目,更新較慢,目前支持到Elasticsearch 2.4。

一、客戶端簡介

你可以使用Java client來執行多種操作:

  • 在一個已經存在的集群中執行標準的index, get, delete 和 search操作。
  • 在一個正在運行的集群中執行管理員任務

獲得一個Client是簡單的。最通用的方式是創建一個TransportClient連接到集群。

maven依賴:

<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>transport</artifactId>
    <version>5.1.2</version>
</dependency>

二、Transport Client

TransportClient遠程連接到一個Elasticsearch集群。它并不加入集群,只是獲得一個或多個初始化transport地址,并且對于每個行為以循環方式與它們通訊(盡管大多數行為將會分成兩段式操作)。

// 啟動時

TransportClient client = new PreBuiltTransportClient(Settings.EMPTY)
        .addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("host1"), 9300))
        .addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("host2"), 9300));

// 關閉時

client.close();

注意,如果你的集群名稱不叫"elasticsearch",那么你必須指定它的名字:

Settings settings = Settings.builder()
        .put("cluster.name", "myClusterName").build();
TransportClient client = new PreBuiltTransportClient(settings);
//Add transport addresses and do something with the client...

Transport client具有一個集群嗅探特性,允許你動態的增加新主機或者移除老主機。當嗅探被激活時,transport client將會連接到內部的節點列表,就是通過addTransportAddress方法構建的節點。然后client將會在這些節點上調用內部的集群狀態API來發現可用的數據節點。內部的節點列表將會被這些數據節點替換。這個列表默認每5秒刷新一次。注意嗅探連接的IP地址是那些在節點的elasticsearch配置中被聲明為發布的地址。

記住,上面的節點列表可能不包活原始的節點,如果這個原始節點不是一個數據節點的話。舉個例子,你初始化時連接到一個主節點,當嗅探后,不會有任何請求再會進入那個主節點,而是其他任意一個數據節點。這樣做的原因是避免搜索流量發送給主節點。

為了啟用嗅探,設置client.transport.snifftrue

Settings settings = Settings.builder()
        .put("client.transport.sniff", true).build();
TransportClient client = new PreBuiltTransportClient(settings);

其他transport client設置如下:

| 參數 | 描述 |
| ------- | ----- | ---- |
| client.transport.ignore_cluster_name | 當設置為true時忽略對節點集群名稱的驗證(0.19.4及以后支持) |
|client.transport.ping_timeout | 等待從一個節點返回ping響應的時間,默認是5秒 |
|client.transport.nodes_sampler_interval | 采樣節點列表并連接的間隔,默認是5秒 |

三、文檔APIs

3.1 索引API

索引API允許你將一個JSON格式的文檔添加到特定的索引中,并使它可以被搜索到。

生成JSON文檔

這里有幾個不同的方式來生產JSON文檔:

  • 人工的拼接成String或者使用byte[]
  • 使用一個Map,它將會自動轉換成相等的JSON
  • 使用第三方的類庫來序列化你的對象,例如Jackson
  • 使用內置的輔助工具XContentFactory.jsonBuilder()

在內部,沒種類型的結果都會轉換成byte[]。如果結果已經是byte[]形式的話,那么會直接使用它。jsonBuilder是高度優化的JSON生成器,會直接構造一個byte[]

1)自己拼接

沒什么說的,根據各API的格式自己寫,注意日期格式問題。

String json = "{" +
        "\"user\":\"kimchy\"," +
        "\"postDate\":\"2013-01-30\"," +
        "\"message\":\"trying out Elasticsearch\"" +
    "}";

2)使用Map

Map<String, Object> json = new HashMap<String, Object>();
json.put("user","kimchy");
json.put("postDate",new Date());
json.put("message","trying out Elasticsearch");

3)使用第三方類庫

以jackson為例。

import com.fasterxml.jackson.databind.*;

// instance a json mapper
ObjectMapper mapper = new ObjectMapper(); // create once, reuse

// generate json
byte[] json = mapper.writeValueAsBytes(yourbeaninstance);

4)使用Elasticsearch輔助工具

import static org.elasticsearch.common.xcontent.XContentFactory.*;

XContentBuilder builder = jsonBuilder()
    .startObject()
        .field("user", "kimchy")
        .field("postDate", new Date())
        .field("message", "trying out Elasticsearch")
    .endObject()

添加文檔到索引

下面的例子將一個JSON文檔添加到名為twitter,類型為tweet的索引中,其id為1。

import static org.elasticsearch.common.xcontent.XContentFactory.*;

IndexResponse response = client.prepareIndex("twitter", "tweet", "1")
        .setSource(jsonBuilder()
                    .startObject()
                        .field("user", "kimchy")
                        .field("postDate", new Date())
                        .field("message", "trying out Elasticsearch")
                    .endObject()
                  )
        .get();

另外一種方式,注意沒有指定id。

String json = "{" +
        "\"user\":\"kimchy\"," +
        "\"postDate\":\"2013-01-30\"," +
        "\"message\":\"trying out Elasticsearch\"" +
    "}";

IndexResponse response = client.prepareIndex("twitter", "tweet")
        .setSource(json)
        .get();

IndexResponse對象將會給你一個報告。

// 索引名稱
String _index = response.getIndex();

// 類型名稱
String _type = response.getType();

// 文檔ID
String _id = response.getId();

// 版本 (如果你是第一次添加這個文檔你將會得到:1)
long _version = response.getVersion();

// 當前實例的狀態
RestStatus status = response.status();

線程化操作

這個將文檔添加到索引的API允許你將操作放在另一個線程中執行(默認的),你可以通過修改operationThreaded的設置為false來關閉它。

3.2 獲取文檔API

這個API允許你根據文檔的ID獲取一個JSON類型的文檔。下面的例子展示的是從twitter索引的tweet類型下獲得ID為1的文檔。

GetResponse response = client.prepareGet("twitter", "tweet", "1").get();

String json = response.getSourceAsString();

與添加文檔到索引的API類似,它默認是在另一個線程中執行獲取文檔操作的,下面的例子可以關閉它。

GetResponse response = client.prepareGet("twitter", "tweet", "1")
        .setOperationThreaded(false)
        .get();

3.3 刪除文檔API

與獲取API很類似,這個API允許你根據文檔的ID刪除一個JSON類型的文檔。下面的例子展示的是從twitter索引的tweet類型下刪除ID為1的文檔。

DeleteResponse response = client.prepareDelete("twitter", "tweet", "1").get();

它默認也是在另一個線程中執行刪除文檔操作的,下面的例子可以關閉它。

DeleteResponse response = client.prepareDelete("twitter", "tweet", "1")
        .setOperationThreaded(false)
        .get();

3.4 根據查詢條件刪除文檔API

這個API可以根據查詢的結果集來批量刪除文檔。

BulkIndexByScrollResponse response =
   DeleteByQueryAction.INSTANCE.newRequestBuilder(client)
       .filter(QueryBuilders.matchQuery("gender", "male")) // 查詢
       .source("persons") // 索引                                 
       .get(); // 執行操作                                            

long deleted = response.getDeleted(); // 被刪除的文檔數量

考慮到它可能是一個耗時很長的操作,如果你想異步的進行此操作參看下面的例子:

DeleteByQueryAction.INSTANCE.newRequestBuilder(client)
   .filter(QueryBuilders.matchQuery("gender", "male")) // 查詢                 
   .source("persons") // 索引                                                 
   .execute(new ActionListener<BulkIndexByScrollResponse>() { // 監聽器         
       @Override
       public void onResponse(BulkIndexByScrollResponse response) {
           long deleted = response.getDeleted(); // 被刪除的文檔數量               
       }
       @Override
       public void onFailure(Exception e) {
           // Handle the exception
       }
   });

3.5 更新文檔API

1)使用UpdateRequest

UpdateRequest updateRequest = new UpdateRequest();
updateRequest.index("index");
updateRequest.type("type");
updateRequest.id("1");
updateRequest.doc(jsonBuilder()
        .startObject()
            .field("gender", "male")
        .endObject());
client.update(updateRequest).get();

2)使用prepareUpdate()

這種方式又有兩個不同的用法。

client.prepareUpdate("ttl", "doc", "1")
        .setScript(new Script("ctx._source.gender = \"male\""  , ScriptService.ScriptType.INLINE, null, null))
        .get();

client.prepareUpdate("ttl", "doc", "1")
        .setDoc(jsonBuilder()               
            .startObject()
                .field("gender", "male")
            .endObject())
        .get();

注意,你不能同時提供script 和 doc

3)upsert

如果待更新文檔還不存在,那么會使用upsert元素來創建一個新文檔。

IndexRequest indexRequest = new IndexRequest("index", "type", "1")
        .source(jsonBuilder()
            .startObject()
                .field("name", "Joe Smith")
                .field("gender", "male")
            .endObject());
UpdateRequest updateRequest = new UpdateRequest("index", "type", "1")
        .doc(jsonBuilder()
            .startObject()
                .field("gender", "male")
            .endObject())
        .upsert(indexRequest);              
client.update(updateRequest).get();

3.6 批量獲得文檔API

你可以根據index, type 和 id來獲得多個文檔。

MultiGetResponse multiGetItemResponses = client.prepareMultiGet()
    .add("twitter", "tweet", "1")           
    .add("twitter", "tweet", "2", "3", "4") 
    .add("another", "type", "foo")          
    .get();

for (MultiGetItemResponse itemResponse : multiGetItemResponses) { 
    GetResponse response = itemResponse.getResponse();
    if (response.isExists()) {                      
        String json = response.getSourceAsString(); 
    }
}

3.7 bulk API

bulk API允許你在單個請求里添加或者刪除多個文檔。下面是一個示例用法:

import static org.elasticsearch.common.xcontent.XContentFactory.*;

BulkRequestBuilder bulkRequest = client.prepareBulk();

// either use client#prepare, or use Requests# to directly build index/delete requests
bulkRequest.add(client.prepareIndex("twitter", "tweet", "1")
        .setSource(jsonBuilder()
                    .startObject()
                        .field("user", "kimchy")
                        .field("postDate", new Date())
                        .field("message", "trying out Elasticsearch")
                    .endObject()
                  )
        );

bulkRequest.add(client.prepareIndex("twitter", "tweet", "2")
        .setSource(jsonBuilder()
                    .startObject()
                        .field("user", "kimchy")
                        .field("postDate", new Date())
                        .field("message", "another post")
                    .endObject()
                  )
        );

BulkResponse bulkResponse = bulkRequest.get();
if (bulkResponse.hasFailures()) {
    // process failures by iterating through each bulk response item
}

3.8 使用Bulk處理器

BulkProcessor類提供了一個簡單的接口來自動的刷新批量操作,它基于請求的數量或者請求的大小或者手動指定一個范圍。

為了使用它,首先需要創建一個BulkProcessor實例。

import org.elasticsearch.action.bulk.BackoffPolicy;
import org.elasticsearch.action.bulk.BulkProcessor;
import org.elasticsearch.common.unit.ByteSizeUnit;
import org.elasticsearch.common.unit.ByteSizeValue;
import org.elasticsearch.common.unit.TimeValue;

BulkProcessor bulkProcessor = BulkProcessor.builder(
        client, // 添加elasticsearch client
        new BulkProcessor.Listener() {
            @Override
            public void beforeBulk(long executionId,
                                   BulkRequest request) { ... } 

            @Override
            public void afterBulk(long executionId,
                                  BulkRequest request,
                                  BulkResponse response) { ... } 

            @Override
            public void afterBulk(long executionId,
                                  BulkRequest request,
                                  Throwable failure) { ... } // 當批處理失敗并且拋出一個異常時
        })
        .setBulkActions(10000) // 每10000個請求作為一批處理
        .setBulkSize(new ByteSizeValue(5, ByteSizeUnit.MB)) // 每5mb寫入一批數據
        .setFlushInterval(TimeValue.timeValueSeconds(5)) // 每5秒寫入一批,不管請求的數量有多少
        .setConcurrentRequests(1) // 請求并發的數量 0表示同時只允許1個請求執行
        .setBackoffPolicy(
            BackoffPolicy.exponentialBackoff(TimeValue.timeValueMillis(100), 3)) // 回退策略
        .build();

其中回退策略初始時會等待100ms,并且指數級增長,重試3次。要想關閉回退需要設置BackoffPolicy.noBackoff()

一些BulkProcessor默認參數:

  • bulkActions 1000
  • bulkSize 5mb
  • 沒有flushInterval
  • concurrentRequests 1
  • backoffPolicy 等待50ms,重試8次,大致最多等待5.1秒

實例完BulkProcessor就可以添加請求:

bulkProcessor.add(new IndexRequest("twitter", "tweet", "1").source(/* your doc here */));
bulkProcessor.add(new DeleteRequest("twitter", "tweet", "2"));

使用完了后要關閉BulkProcessor

// 10分鐘后關閉
bulkProcessor.awaitClose(10, TimeUnit.MINUTES);

// 立即關閉
bulkProcessor.close();

如果在10分鐘內所有的請求執行完畢,awaitClose方法返回true,否則返回false。這兩個方法都會將剩下的文檔寫入,如果設置了flushInterval則會禁用其他的計劃寫入。

測試時使用Bulk Processor

如果在測試時你想使用BulkProcessor填充你的數據集你最好將concurrentRequests設置為0:

BulkProcessor bulkProcessor = BulkProcessor.builder(client, new BulkProcessor.Listener() { /* Listener methods */ })
        .setBulkActions(10000)
        .setConcurrentRequests(0)
        .build();

// 添加你的請求
bulkProcessor.add(/* Your requests */);

// 寫入剩余的請求
bulkProcessor.flush();

// 關閉
bulkProcessor.close();

// 刷新你的索引
client.admin().indices().prepareRefresh().get();

// 現在你可以開始搜索
client.prepareSearch().get();

四、Query DSL

Elasticsearch提供了一個基于JSON的Query DSL(domain specific languages)來定義查詢。它由兩種類型的從句組成:

1)葉子查詢從句

葉子查詢從句在一個指定的域里尋找指定的值。例如match, term 或者 range查詢。這些查詢可以單獨使用。

2)復合查詢從句

復合查詢從句包裝了其他葉子查詢子句或者復合查詢從句,被用在一個邏輯范式里聯合多條件查詢(例如:bool 或者 dis_max查詢),或者改變它們的行為(例如:constant_score查詢)。

查詢構建器的工廠類是QueryBuilders,一旦你的查詢準備好后,你就可以使用搜索API了。

要想使用QueryBuilders,你只需要將它們導入進你的類即可:

import static org.elasticsearch.index.query.QueryBuilders.*;

你可以使用QueryBuilder對象的toString()方法輕松的將生成的JSON查詢條件打印出來。

QueryBuilder可以用在任何接受查詢條件的API里,例如countsearch

4.1 Match All Query

最簡單的查詢,匹配所有的文檔,并將_score的值設置為1.0。

QueryBuilder qb = matchAllQuery();

4.2 全文查詢

高層次的全文查詢通常被用來在文本域里執行全文查詢并返回相關性最強的結果,比如一封電子郵件的正文里。Elasticsearch中的相關性概念非常重要,也是完全區別于傳統關系型數據庫的一個概念,數據庫中的一條記錄要么匹配要么不匹配。

這個類別下的查詢有:

match query

這是執行全文查詢的標準方式, 包括fuzzy matching(模糊匹配),短語或者近似查詢。

multi_match query

多字段版本匹配查詢

common_terms query

一個更專業化的查詢,更適合一些不尋常的詞語。它是stopwords的一個現代化替代。

query_string query

支持Lucene查詢字符串語法協議,允許你指定AND|OR|NOT條件和在一個單獨的查詢字符串里進行多字段搜索。只建議高級用戶使用。

simple_query_string

一個更簡單的,更穩定的query_string版本

Match Query

QueryBuilder qb = matchQuery(
    "name",                   // 文檔域的名稱
    "kimchy elasticsearch"    // 要搜索的文本
);

Multi Match Query

QueryBuilder qb = multiMatchQuery(
    "kimchy elasticsearch", // 要搜索的文本
    "user", "message"       // 文檔域的名稱
);

Common Terms Query

QueryBuilder qb = commonTermsQuery("name",    // 文檔域的名稱
                                   "kimchy"); // 值

Query String Query

QueryBuilder qb = queryStringQuery("+kimchy -elasticsearch");  // 要搜索的文本

Simple Query String Query

QueryBuilder qb = simpleQueryStringQuery("+kimchy -elasticsearch");  // 要搜索的文本

4.3 術語級別查詢(Term level queries)

全文查詢會在執行前分析查詢字符串,術語級別的查詢會在索引中精確匹配要查詢的詞語。

這些查詢通常被用在結構化的數據上,比如數字、日期和一些字典表類的數據,而不是一堆很長的文本。另外,它們還允許你手工處理低等級查詢。

這個組里有如下查詢:

term query

在指定域里精確的查詢包含指定詞語的文檔。

QueryBuilder qb = termQuery(
    "name",    // 文檔域的名稱
    "kimchy"   // 要搜索的詞
);

terms query

在指定域里精確的查詢包含任一指定詞語的文檔。

QueryBuilder qb = termsQuery("tags",    // 文檔域的名稱
    "blue", "pill");                    // 要搜索的詞

range query

查詢指定域的值(日期、數字或者字符串)在指定范圍內的文檔。

QueryBuilder qb = rangeQuery("price")   // 文檔域的名稱
    .from(5)                            // 范圍的開始
    .to(10)                             // 范圍的結束
    .includeLower(true)                 // 包括范圍的開始
    .includeUpper(false);               // 不包括范圍的結束

exists query

查詢指定域里有不是null值的所有文檔。

QueryBuilder qb = existsQuery("name");       // 文檔域的名稱

prefix query

查詢所有指定域的值包含特定前綴的文檔

QueryBuilder qb = prefixQuery(
    "brand",    // 文檔域的名稱
    "heine"     // 前綴
);

wildcard query

查詢所有指定域的值與通配符表達式匹配的文檔。支持的通配符有單字符通配符?和多字符通配符*

QueryBuilder qb = wildcardQuery("user", "k?mc*");

regexp query

查詢所有指定域的值與正則表達式匹配的文檔。

QueryBuilder qb = regexpQuery(
    "name.first",        // 文檔域的名稱
    "s.*y");             // 正則表達式

fuzzy query

過時的,針對字符串域,它將會被移除而且沒有替代功能

查詢所有指定域的值與指定的術語相近的文檔。模糊性是由Levenshtein(編輯距離算法)編輯距離1或者2來衡量。

QueryBuilder qb = fuzzyQuery(
    "name",     // 文檔域的名稱
    "kimzhy"    // 搜索文本
);

type query

查詢制定類型的所有文檔

QueryBuilder qb = typeQuery("my_type");

ids query
查詢指定類型與ID的文檔

QueryBuilder qb = idsQuery("my_type", "type2")
    .addIds("1", "4", "100");

QueryBuilder qb = idsQuery() // 類型是可選的
    .addIds("1", "4", "100");

4.4 復合查詢(Compound queries)

復合查詢包裝了其他復合或者葉子查詢,用來合并它們的結果和分數,改變它們的行為,或者從查詢切換到過濾器上下文。

本組有如下查詢:

constant_score query

這個查詢包裹了另一個查詢,但是在過濾器上下文執行它。所有匹配的文檔都會被賦予一個相同的_score。

QueryBuilder qb = constantScoreQuery(
        termQuery("name","kimchy")      // 查詢語句
    )
    .boost(2.0f);                       // 分數

bool query

默認的復合查詢,具體有must, should, must_not, 或者filter從句。mustshould從句會將它們的分數相加,越多的匹配條件分數越高。

QueryBuilder qb = boolQuery()
    .must(termQuery("content", "test1"))    // must query
    .must(termQuery("content", "test4"))    
    .mustNot(termQuery("content", "test2")) // must not query
    .should(termQuery("content", "test3"))  // should query
    .filter(termQuery("content", "test5")); // 與一般查詢作用一樣,只不過不參與評分

dis_max query

這種查詢接受多個子查詢,并且返回所有子查詢的結果。與bool query不同的是,它會使用最匹配子查詢的分數。

QueryBuilder qb = disMaxQuery()
    .add(termQuery("name", "kimchy"))        
    .add(termQuery("name", "elasticsearch")) 
    .boost(1.2f)                             
    .tieBreaker(0.7f);                       

五、搜索APIs

搜索API允許你執行一個搜索查詢,并且取回查詢匹配的數據,查詢條件在后面的章節介紹。它可以被執行在1個或多個索引和類型上。這里有一個例子:

import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.search.SearchType;
import org.elasticsearch.index.query.QueryBuilders.*;

SearchResponse response = client.prepareSearch("index1", "index2")
        .setTypes("type1", "type2")
        .setSearchType(SearchType.DFS_QUERY_THEN_FETCH)
        .setQuery(QueryBuilders.termQuery("multi", "test"))                 // Query
        .setPostFilter(QueryBuilders.rangeQuery("age").from(12).to(18))     // Filter
        .setFrom(0).setSize(60).setExplain(true) // 分頁參數
        .get();

注意,所有的參數都是可選的。下面是個條件最少的搜索:

// 使用默認參數匹配整個集群所有的文檔
SearchResponse response = client.prepareSearch().get();

盡管Java API定義了附加的searchType:QUERY_AND_FETCHDFS_QUERY_AND_FETCH,這些模式是內部優化用的,用戶不應該在API里使用它們。

實際使用中的常見問題

我是將客戶端與Spring集成后使用的。期間遇到了一些問題特此記錄下。

1)找不到Log4j 2的相關方法

因為我項目本身就是Log4j 2所以不用做什么配置。用其他日志框架的可以參考這里

但是我第一次啟動時提示NoSuchMethodException,后來嘗試把Log4j 2的版本升高一些解決這個問題了。原來使用的是2.0.2升級到2.7。

2)failed to get node info for [#transport#-1]

Elasticsearch服務器安裝好后運行起來,通過瀏覽器可以訪問,通過HTTP的接口也正常。上網搜索后發現HTTP接口的默認端口號是9200,但是TransportClient默認的端口號是9300。

未完,待續...

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,748評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,165評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,595評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,633評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,435評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,943評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,035評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,175評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,713評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,599評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,788評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,303評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,034評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,412評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,664評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,408評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,747評論 2 370

推薦閱讀更多精彩內容