作者: 1chigua

  • 深入讨论几种 ES Mapping 修改方法和局限

    https://www.bennhuang.com/posts/es-mapping/

    前言

    众所周知,ElasticSearch 中的 Mapping 决定了字段的索引方式。当一个字段的索引类型确定后就无法修改。然而在日常开发中,忘记更新 mapping 是偶尔会遇到的,但修改 mapping 却是一件不容易的事情。这篇文章不仅详细讨论了 reindex 这种常见解决方法,包括 reindex 的停机和数据丢失问题、以及在各种情况下规避上述问题的思路和方法,还介绍了另外两种更加低成本的解决方法。

    一个常见的错误

    “发布测试环境前我竟然忘记修改 mapping 了?”

    {"error":{"root_cause":[{"type":"illegal_argument_exception","reason":"Fielddata is disabled on text fields by default. Set fielddata=true on [player_type] in order to load fielddata in memory by uninverting the inverted index......
    

    所以我要如何快速解决这个问题呢? 所以我要如何快速修改 mapping 中一个字段的索引类型呢?

    Reindex

    “即使对 reindex 烂熟于心,也需要留意停机时间和数据丢失……”

    Reindex 几乎是这类问题的标准答案,其中一个常见思路是:创建一个新的 index-tmp,然后把数据 reindex 到这个新 index-tmp,再把原来的 index 删除重建、设置正确的 mapping,最后把数据重新从 index-tmp 中 reindex 回来。很明显这个思路除了操作危险外,还会有较长的停机时间。

    ElasticSearch 官方博客里曾介绍过另一种方法,只要后端业务以 alias 的方式访问 index,就可以做到完全无停机时间(Zero-Downtime)。具体思路是:首先创建一个 mapping 正确的新 index-v2,把数据 reindex 到这个新 index-v2,然后直接把 alias 重命名到这个index-v2。这样可以让后端业务直接访问到新的 index-v2,达到后端业务对 reindex 操作无感知的效果。

    然而这种方法也有不足,官方博客里完全没有提及数据丢失的问题。如果线上写入操作频繁,在 reindex 和更新 alias 步骤中有一段时间窗口,新数据依然会写入到即将弃用的旧 index,造成新 index-v2 丢失该部分数据的问题。即使采用官博推荐的 bulk API,也无法在原理上根本地规避问题。

    因此需要结合实际的业务情况,做一些因地制宜的权衡。

    1. 那就丢亿点点数据

    有时候,丢失一些临时数据也是可以接受的,比如测试环境的PV上报、用来时间窗口统计的热词记录……对于这些可丢失数据,当我们发出那句灵魂拷问,“数据重要还是我重要下班重要!”,相信产品经理也是会认真考虑的。

    2. 找个夜黑风高的时候停机

    防止新数据丢失最简单的办法就是停机了。只要没有写入,哪来数据丢失。在我常常遇到的 ES 使用场景里,新数据总是通过消费者从其他数据源里同步过来,只要先让消费者暂停就可以解决问题。后端业务还是可以正常读取 ES,只是停止了消费者的写入。当一切结束后,消费者可以直接从消息队列里获取堆积的写入任务,轻松地恢复工作。

    3. 让新数据先走

    能不能在不停机的情况下,也能做到不丢失数据?完全可以。创建新 index 后,立即重新设置 alias,让新 index 先开始接受新数据,然后再把老数据 reindex 过来。这样数据要么在新 index、要么在旧 index,但最终都会写入在新 index,解决了新文档丢失的问题。

    但我也想到了这种方法的几个不足和局限:

    1. 中间会有很长一段时间(当新 index 开始接客、旧数据还没有全部 reindex 过来时),后端业务将无法访问到旧数据。这可能会对业务造成影响,比如用户突然无法搜索到以前的文章。
    2. 这个方法只能保证新文档插入不丢失,对旧文档的更新和删除操作依然可能丢失。当然,如果业务里 ES 的使用场景是“只写不改”,那也就不是问题了。不过说起来,把 ES 用来作为 CRUD 数据库确实是很奇怪的技术选型~

    4. 两步 reindex

    我还想到了一个方法,可以在不停机的情况下,既让后端业务尽可能正常地访问老数据,又不丢失新文档的创建。这个方法要求文档创建时附带一个 created_at 字段。

    1. 创建一个新的 index-v2,设置好需要的 mapping。
    PUT /index-v2/_doc/_mapping
    {
    	"properties": <all_you_need>
    }
    
    1. 先“悄悄”准备老数据,比如先 reindex 某个时间点之前的老数据(比如一小时前)。
    POST _reindex
    {
    	"source": {
    		"index": "index-v1",
    		"query": {
    			"range": {
    				"created_at": {
    					"lt": <一个时间点>
    				}
    			}
    		}
    	},
    	"dest": {
    		"index": "index-v2"
    	}
    }
    
    1. 当老数据准备好后,立即修改 alias,让新 index-v2 开始接受新数据。
    POST /_aliases
    {
    	"actions": [
    		{
    			"remove": {
    				"alias": "index",
    				"index": "index-v1"
    			}
    		},
    		{
    			"add": {
    				"alias": "index",
    				"index": "index-v2"
    			}
    		}
    	]
    }
    
    1. 再将这个时间点之后的数据 reindex 到新 index-v2
    POST _reindex
    {
    	"source": {
    		"index": "index-v1",
    		"query": {
    			"range": {
    				"created_at": {
    					"gte": <一个时间点>
    				}
    			}
    		}
    	},
    	"dest": {
    		"index": "index-v2"
    	}
    }
    

    这样在迁移过程中,既不需要停机,也不会丢失新数据,也不会出现长时间无法访问历史数据的问题。后端业务几乎可以访问到所有的数据,只有在第二次 reindex 期间会有极小部分数据在短时间内无法访问(不可见数据范围大约是 created_at: [the_timestamp, now)),但也会很快地恢复。因为数据量非常少,第二次 reindex 速度很快,等到结束后,后端业务就可以完全正常地访问到所有的数据。

    这个方法可以保证新文档插入不丢失、大部分旧文档更新删除不丢失,但在第二次 reindex 期间那些不可见文档的更新、删除操作依然有丢失的可能。这个方法在不停机的前提下大幅地缩小了负面影响的范围。

    小结

    以上只是尽可能地讨论了 reindex 在实际业务中可能遇到的问题,以及可行的方法与思路。虽然在实际应用中,大多数情况都可以用简单直接的方法解决,比如遗弃数据和停机,但认识更多情况可以规避潜在的决策风险。

    对了,这里没有讨论 reindex 优化,我感觉这是另外一个话题了。我看到有些高赞文章提到通过配置来增大 reindex 单次索引的文档数量,或者通过 scroll 并发来加快索引。我直观的感觉,这些方法在停机情况下应该是不错的做法,但在不停机时可能会给集群带来更多的压力,尤其是 index 数据量很大、读写频繁的场景,可能会影响到正常的业务。在不停机的情况,也许我们应该更多考虑的不是如何加快 reindex 过程,而是尽可能降低 reindex 对正常业务的影响。

    除了 reindex,还有其他一些方法可以“修改”已有字段的索引类型。

    新字段替换

    “既然不能直接修改原来字段的索引类型,那我重新建个字段好吧……”

    很多时候,为了一点点错误索引的数据而 reindex 整个 index,也许是一件大动干戈的事情。利用其他技巧可以更加低成本的解决问题,比如建个新字段。

    1. 在 mapping 添加新字段 new_field,设置好需要的索引方式
    PUT /my-index/_doc/_mapping
    {
      "properties": {
        "new_field": {
          "type": "keyword"
        }
      }
    }
    
    1. 将原来文档的旧字段的值重新赋予到新字段
    POST /my-index/_update_by_query?conflicts=proceed
    {
      "script": {
        "source": "ctx._source.new_field = ctx._source.old_field",
        "lang": "painless"
      },
      "query": {
        "exists": { "field": "old_field" }
      }
    }
    
    1. 在业务代码中弃用原来的字段

    小结

    这个方法非常适合低成本解决少量数据被错误索引的情况,比如在发布前忘记修改 mapping。但是如果需要修改索引类型的数据很多,这个方法需要更新大量已有数据,可能会对集群带来一定压力。毕竟在 ElasticSearch 里,所谓的文档修改就是先标记删除、然后重新插入,如果要更新整个 index 的所有文档,也许还不如 reindex 来得痛快……

    采用 multi-field

    “作为一个成熟的 DB,总是可以为一个字段建立多种索引……”

    ElasticSearch 的 multi-field 特性,让同一个字段可以有多种不同的索引类型。我们可以利用这个特性在 mapping 中为老字段追加新的索引类型。比如这个例子:

    PUT /my-index/_doc/_mapping
    {
      "properties": {
        "company": {
          "type": "keyword",
          "fields": {
            "name": {
              "type": "text"
            }
          }
        }
      }
    }
    

    原本 company 字段采用了 keyword 的索引类型,只能用于数值匹配。在上面的例子中,为 company 字段追加了另一种支持全文搜索的索引类型 text,然后把这个全文搜索版本取名叫 company.name。这样查询条件 { "company.name": "腾讯" } 就可以匹配到 { "company": "腾讯科技有限公司" } 的数据了。

    因为本质上只是对同一个字段添加不同的索引,实际文档(documents)中并不会真的多出来一个叫 company.name 的字段,插入数据时也不需要为 company.name 赋值。一切索引工作都由 ElasticSearch 自动完成。

    然而用这种方法修改 mapping 后,只有新的写入才会让该文档的新索引生效。也就是说,只有插入新的文档、或者更新旧文档时,ElasticSearch 才会给该文档重新索引、将该文档写入新索引。除非老数据有更新,用 company.name 无法搜索到老数据,因为老数据根本不在这个索引里!

    但问题总是可以解决,不就是还差一次全局更新嘛~(坏笑脸

    // 这个更新没有任何意义,纯粹是为了触发老数据的重新索引
    POST /my-index/_update_by_query?conflicts=proceed
    {
      "script": {
        "source": "ctx",
        "lang": "painless"
      },
      "query": {
        "exists": { "field": "company" }
      }
    }
    

    小结

    这个方法不仅适合低成本解决少量数据被错误索引的情况,而且在同个字段需要多种索引方式(尤其需要是搭配不同的分析器)时这几乎是唯一的做法,因为 multi-field 就是为了这种场景准备的。但这个方法同样需要注意更新数据的规模,以及对性能的影响……

    最后

    我在中英互联网上搜索时,发现很难找到详细讨论这个话题的文章。其中我找到最好的资料是 ElasticSearch 官博的一篇文章,里面简要介绍了这三种方法,但是很少介绍各个方法的局限、已知问题和适用场景,比如几乎没有提到 reindex 的数据丢失问题。我还找到了很多只言片语,它们大多只是简单提供了一个解决方法,却很少介绍方法的用意和局限。

    所以我尝试写篇文章,想着结合自己的认识和理解,试着更加全面地讨论一下各个方法的利弊权衡,看看能不能从这个常见的小问题出发,窥探到一点点原理和设计的影子,填补一点点底层技术与实际业务的缝隙,引发一些可能的讨论和思路。

    最后不得不感叹:方法常有,银弹难寻!只有根据实际情况,在业务可接受的影响范围内,找到最简单方法,往往那就是最佳实践。

    参考

  • 一文搞懂 Elasticsearch 之 Mapping 

    这篇文章主要介绍 Mapping、Dynamic Mapping 以及 ElasticSearch 是如何自动判断字段的类型,同时介绍 Mapping 的相关参数设置。

    首先来看下什么是 Mapping:

    什么是 Mapping?#

    一篇文章带你搞定 ElasticSearch 术语中,我们讲到了 Mapping 类似于数据库中的表结构定义 schema,它有以下几个作用:

    • 定义索引中的字段的名称
    • 定义字段的数据类型,比如字符串、数字、布尔
    • 字段,倒排索引的相关配置,比如设置某个字段为不被索引、记录 position 等

    在 ES 早期版本,一个索引下是可以有多个 Type 的,从 7.0 开始,一个索引只有一个 Type,也可以说一个 Type 有一个 Mapping 定义。

    在了解了什么是 Mapping 之后,接下来对 Mapping 的设置做下介绍:

    Mapping 设置#

    CopyPUT users { "mappings": { "_doc": { "dynamic": false } } }

    在创建一个索引的时候,可以对 dynamic 进行设置,可以设成 falsetrue 或者 strict

    比如一个新的文档,这个文档包含一个字段,当 Dynamic 设置为 true 时,这个文档可以被索引进 ES,这个字段也可以被索引,也就是这个字段可以被搜索,Mapping 也同时被更新;当 dynamic 被设置为 false 时候,存在新增字段的数据写入,该数据可以被索引,但是新增字段被丢弃;当设置成 strict 模式时候,数据写入直接出错。

    另外还有 index 参数,用来控制当前字段是否被索引,默认为 true,如果设为 false,则该字段不可被搜索。

    参数 index_options 用于控制倒排索引记录的内容,有如下 4 种配置:

    • doc:只记录 doc id
    • freqs:记录 doc id 和 term frequencies
    • positions:记录 doc idterm frequencies 和 term position
    • offsets:记录 doc idterm frequenciesterm position 和 character offects

    另外,text 类型默认配置为 positions,其他类型默认为 doc,记录内容越多,占用存储空间越大。

    null_value 主要是当字段遇到 null 值时的处理策略,默认为 NULL,即空值,此时 ES 会默认忽略该值,可以通过设定该值设定字段的默认值,另外只有 KeyWord 类型支持设定 null_value

    copy_to 作用是将该字段的值复制到目标字段,实现类似 _all 的作用,它不会出现在 _source 中,只用来搜索。

    除了上述介绍的参数,还有许多参数,大家感兴趣的可以在官方文档中进行查看。

    在学习了 Mapping 的设置之后,让我们来看下字段的数据类型有哪些吧!

    字段数据类型#

    ES 字段类型类似于 MySQL 中的字段类型,ES 字段类型主要有:核心类型、复杂类型、地理类型以及特殊类型,具体的数据类型如下图所示:

    核心类型#

    从图中可以看出核心类型可以划分为字符串类型、数字类型、日期类型、布尔类型、基于 BASE64 的二进制类型、范围类型。

    字符串类型#

    其中,在 ES 7.x 有两种字符串类型:text 和 keyword,在 ES 5.x 之后 string 类型已经不再支持了。

    text 类型适用于需要被全文检索的字段,例如新闻正文、邮件内容等比较长的文字,text 类型会被 Lucene 分词器(Analyzer)处理为一个个词项,并使用 Lucene 倒排索引存储,text 字段不能被用于排序,如果需要使用该类型的字段只需要在定义映射时指定 JSON 中对应字段的 type 为 text

    keyword 适合简短、结构化字符串,例如主机名、姓名、商品名称等,可以用于过滤、排序、聚合检索,也可以用于精确查询

    数字类型#

    数字类型分为 long、integer、short、byte、double、float、half_float、scaled_float

    数字类型的字段在满足需求的前提下应当尽量选择范围较小的数据类型,字段长度越短,搜索效率越高,对于浮点数,可以优先考虑使用 scaled_float 类型,该类型可以通过缩放因子来精确浮点数,例如 12.34 可以转换为 1234 来存储。

    日期类型#

    在 ES 中日期可以为以下形式:

    • 格式化的日期字符串,例如 2020-03-17 00:00、2020/03/17
    • 时间戳(和 1970-01-01 00:00:00 UTC 的差值),单位毫秒或者秒

    即使是格式化的日期字符串,ES 底层依然采用的是时间戳的形式存储。

    布尔类型#

    JSON 文档中同样存在布尔类型,不过 JSON 字符串类型也可以被 ES 转换为布尔类型存储,前提是字符串的取值为 true 或者 false,布尔类型常用于检索中的过滤条件。

    二进制类型#

    二进制类型 binary 接受 BASE64 编码的字符串,默认 store 属性为 false,并且不可以被搜索。

    范围类型#

    范围类型可以用来表达一个数据的区间,可以分为5种:integer_range、float_range、long_range、double_range 以及 date_range

    复杂类型#

    复合类型主要有对象类型(object)和嵌套类型(nested):

    对象类型#

    JSON 字符串允许嵌套对象,一个文档可以嵌套多个、多层对象。可以通过对象类型来存储二级文档,不过由于 Lucene 并没有内部对象的概念,ES 会将原 JSON 文档扁平化,例如文档:

    Copy{ "name": { "first": "wu", "last": "px" } }

    实际上 ES 会将其转换为以下格式,并通过 Lucene 存储,即使 name 是 object 类型:

    Copy{ "name.first": "wu", "name.last": "px" }

    嵌套类型#

    嵌套类型可以看成是一个特殊的对象类型,可以让对象数组独立检索,例如文档:

    Copy{ "group": "users", "username": [ { "first": "wu", "last": "px"}, { "first": "hu", "last": "xy"}, { "first": "wu", "last": "mx"} ] }

    username 字段是一个 JSON 数组,并且每个数组对象都是一个 JSON 对象。如果将 username 设置为对象类型,那么 ES 会将其转换为:

    Copy{ "group": "users", "username.first": ["wu", "hu", "wu"], "username.last": ["px", "xy", "mx"] }

    可以看出转换后的 JSON 文档中 first 和 last 的关联丢失了,如果尝试搜索 first 为 wulast 为 xy 的文档,那么成功会检索出上述文档,但是 wu 和 xy 在原 JSON 文档中并不属于同一个 JSON 对象,应当是不匹配的,即检索不出任何结果。

    嵌套类型就是为了解决这种问题的,嵌套类型将数组中的每个 JSON 对象作为独立的隐藏文档来存储,每个嵌套的对象都能够独立地被搜索,所以上述案例中虽然表面上只有 1 个文档,但实际上是存储了 4 个文档。

    地理类型#

    地理类型字段分为两种:经纬度类型和地理区域类型:

    经纬度类型#

    经纬度类型字段(geo_point)可以存储经纬度相关信息,通过地理类型的字段,可以用来实现诸如查找在指定地理区域内相关的文档、根据距离排序、根据地理位置修改评分规则等需求。

    地理区域类型#

    经纬度类型可以表达一个点,而 geo_shape 类型可以表达一块地理区域,区域的形状可以是任意多边形,也可以是点、线、面、多点、多线、多面等几何类型。

    特殊类型#

    特殊类型包括 IP 类型、过滤器类型、Join 类型、别名类型等,在这里简单介绍下 IP 类型和 Join 类型,其他特殊类型可以查看官方文档。

    IP 类型#

    IP 类型的字段可以用来存储 IPv4 或者 IPv6 地址,如果需要存储 IP 类型的字段,需要手动定义映射:

    Copy{ "mappings": { "properties": { "my_ip": { "type": "ip" } } } }

    Join 类型#

    Join 类型是 ES 6.x 引入的类型,以取代淘汰的 _parent 元字段,用来实现文档的一对一、一对多的关系,主要用来做父子查询。

    Join 类型的 Mapping 如下:

    CopyPUT my_index { "mappings": { "properties": { "my_join_field": { "type": "join", "relations": { "question": "answer" } } } } }

    其中,my_join_field 为 Join 类型字段的名称;relations 指定关系:question 是 answer 的父类。

    例如定义一个 ID 为 1 的父文档:

    CopyPUT my_join_index/1?refresh { "text": "This is a question", "my_join_field": "question" }

    接下来定义一个子文档,该文档指定了父文档 ID 为 1:

    CopyPUT my_join_index/_doc/2?routing=1&refresh { "text": "This is an answer", "my_join_field": { "name": "answer", "parent": "1" } }

    再了解完字段数据类型后,再让我们看下什么是 Dynamic Mapping?

    什么是 Dynamic Mapping?#

    Dynamic Mapping 机制使我们不需要手动定义 Mapping,ES 会自动根据文档信息来判断字段合适的类型,但是有时候也会推算的不对,比如地理位置信息有可能会判断为 Text,当类型如果设置不对时,会导致一些功能无法正常工作,比如 Range 查询。

    类型自动识别#

    ES 类型的自动识别是基于 JSON 的格式,如果输入的是 JSON 是字符串且格式为日期格式,ES 会自动设置成 Date 类型;当输入的字符串是数字的时候,ES 默认会当成字符串来处理,可以通过设置来转换成合适的类型;如果输入的是 Text 字段的时候,ES 会自动增加 keyword 子字段,还有一些自动识别如下图所示:

    下面我们通过一个例子是看看是怎么类型自动识别的,输入如下请求,创建索引:

    CopyPUT /mapping_test/_doc/1 { "uid": "123", "username": "wupx", "birth": "2020-03-16", "married": false, "age": 18, "heigh": 180, "tags": [ "java", "boy" ], "money": 999.9 }

    然后使用 GET /mapping_test/_mapping 查看,结果如下图所示:

    可以从结果中看出,ES 会根据文档信息自动推算出合适的类型。

    哦豁,万一我想修改 Mapping 的字段类型,能否更改呢?让我们分以下两种情况来探究下:

    修改 Mapping 字段类型?#

    如果是新增加的字段,根据 Dynamic 的设置分为以下三种状况:

    • 当 Dynamic 设置为 true 时,一旦有新增字段的文档写入,Mapping 也同时被更新。
    • 当 Dynamic 设置为 false 时,索引的 Mapping 是不会被更新的,新增字段的数据无法被索引,也就是无法被搜索,但是信息会出现在 _source 中。
    • 当 Dynamic 设置为 strict 时,文档写入会失败。

    另外一种是字段已经存在,这种情况下,ES 是不允许修改字段的类型的,因为 ES 是根据 Lucene 实现的倒排索引,一旦生成后就不允许修改,如果希望改变字段类型,必须使用 Reindex API 重建索引。

    不能修改的原因是如果修改了字段的数据类型,会导致已被索引的无法被搜索,但是如果是增加新的字段,就不会有这样的影响。

    总结

    本文主要介绍了 Mapping 和 Dynamic Mapping,同时对字段类型做了详细介绍,也介绍了在 ES 中是如何对字段类型做推算的,了解了 Mapping 的相关参数设置。

    在公众号【武培轩】回复【es】获取思维导图以及源代码。

    参考文献

    《Elasticsearch技术解析与实战》

    Elastic Stack从入门到实践

    Elasticsearch核心技术与实战

    https://www.elastic.co/guide/en/elasticsearch/reference/7.1/mapping.html

  • 页面自动滚动

    https://doc.yonyoucloud.com/doc/wiki/project/javascript-special-efficacy-examples/page-automatically-scroll.html

    https://pdf.bookhub.tech/books.html

    实例说明

    本实例实现在打开页面,当页面出现纵向滚动条时,页面中的内容将从上向下进行滚动。

    技术要点

    本例主要是使用 window 对象的 scroll()方法指定窗口的当前位置。下面对 scroll()方法进行详细说明。

    scroll()方法的语法格式:

    scroll(x,y); 

    参数说明如下。

    1. x:屏幕的横向坐标
    2. y:屏幕的纵向坐标

    功能:指定窗口滚动坐标的位置。

    实现过程

    用于实现功能的主页面 index.html。

    <!DOCTYPE html>  
    <html>  
    <head>  
        <meta charset="utf-8">  
        <title></title>  
        <script type="text/javascript">  
        var position = 0;  
        function scroller()  
        {  
            if(true)  
            {  
                position++;  
                scroll(0,position);  
                clearTimeout(timer);  
                var timer = setTimeout("scroller()",10);  
            }  
        }  
        scroller();  
        </script>  
    </head>  
    <body>  
        <img src="new.jpg"/><br/>  
        <img src="new.jpg"/><br/>  
        <img src="new.jpg"/><br/>  
        <img src="new.jpg"/><br/>  
    </body>  
    </html>  
  • 关于运营边界的思考

    这几天有个工作3年的的运营朋友向我咨询如何转岗到产品经理。他表达了一下对运营的理解,他认为运营需要极强的文案水平,并且对运营的前途感到悲观。

    我挺诧异他会有这样的想法,在他看来,产品经理的岗位是远好于运营岗位的。

    从企业薪资上说,确实,同年的产品平均比运营高不少。并且关于产品经理改变世界的理论深入人心,如果没有深入了解过产品和运营的工作和方法论,很容易产生运营不如产品的想法。

    所以我这次结合王诗沐的《幕后产品》抛砖引玉聊一聊运营的边界和价值在哪里。

    诗沐将运营分成了三个层次:经营用户流量、经营资源、经营价值链。我觉得这个划分非常好,但是他写得有点疏漏,我补充了一下,仍然沿用他的框架。

    1、基础阶段:经营用户流量

    什么是经营用户流量?

    我们通俗意义上理解的运营,比如活动运营、用户运营、产品运营等等。这些所有的细分岗位所做的事情,就是经营用户流量。其实就是结合用户生命周期管理和AARRR模型所做的所有事情。

    这方面已经有很成熟的理论和岗位划分了,我大致总结一下:

    1、引入期:研究用户是谁,他们在哪里

    这是最基本的研究寻找用户的方法。运营的资源或者说公司的资源是有限的,获取海量用户固然美好,但是现实往往很残酷。我们需要找到最想要最精准的用户,然后把他们带到产品里。

    以诗沐的网易云音乐为例,他们认为大学生是网易云最想吸引的群体,定位这个目标后,剩下的就是寻找他们在哪里。理所应当,他们都在国内的各大高校。这两个确定之后,剩下的就是用运营手段去吸引他们到产品里。

    对于我们其他互联网产品或者游戏来说,也同样如此。

    比如你的目标用户是二次元动漫用户,那B站就是一个不错的地方(可能也有其他的好地方,但是集中资源先在B站测试肯定不会错),你可以投放广告,自制视频,软文等来吸引用户。

    这里的投放广告、制作视频、软文等,其实就是运营在这个阶段要负责的事,比如视频,可能不需要运营,有专业的设计师来做,但是运营是要对此负责的,视频的剧本设计到效果验收,都需要运营的参与。

    扩量阶段:

    投放广告、用户推荐、裂变传播策略等,这些同样是运营需要负责的事情,只不过会对应到各细分的岗位。

    2、成长期:激活用户

    这里其实就是寻找增长黑客理论中的aha时刻,让用户迅速感知到产品价值,从新用户转化成活跃用户。

    从如何寻找aha时刻到运用哪些手段促成用户转化,也是运营的负责范围。

    比如电商平台,成功完成一笔订单可能是它的aha时刻,那平台在新用户的转化激活层面会做什么?商家运营会要求商家给一定的折扣,活动运营会做一些新用户的促销活动。平台本身会提供一些门槛较低的优化券来促成用户转化。这些全部是运营的负责范围。

    3、成熟期:提升留存/收入

    找到用户的aha时刻,这个阶段其实就是用各种运营和产品手段促使用户完成更多这样的行为。

    比如电商,就是利用DMP后台和RFM等理论将用户进行分层,利用短信push、优惠券、促销活动等各种手段提升用户的复购率。

    4、休眠/流失期:用户唤醒/召回

    这个阶段需要定义流失用户特征,建立用户流失预测模型和用户唤醒召回策略等。

    以游戏为例,粗略说一下游戏的RFM用户分层策略。如果用户游戏充值1万或以上,则进入鲸鱼用户池。在该池内的用户若连续一周未登录或未充值则需要预警。鲸鱼用户客服可以依次通过游戏内消息、QQ、短信、电话等形式回访,和用户进行沟通。

    2、高级阶段:经营资源

    1,识别资源

    这个对于运营来说很关键。对于大部分公司来说,缺预算、缺技术、缺数据等缺各种东西是非常常见的情况,在缺各种东西的情况下,能找到合适的支持,把事儿做成,更凸显运营能力。

    比如诗沐在书中举了个例子:

    有的网红公司在起步时做撮合网红和品牌对接的生意,对于刚起步的公司来说,网红资源和品牌资源都很少,如果只看到这两块资源,则很难破局,会陷入一个“先有鸡还是先有蛋”的难题。但如果换一个视角,公司是否一定要签约网红才算拥有网红资源呢?其实未必,与网红合作也能达到一定的效果。网红拥有粉丝流量,品牌想在网红身上投放广告,但网红未必擅长品牌对接,商务合作的事情,因此可以将商务谈判能力视为资源,然后寻找缺乏这个资源的网红,再打包寻找有投放广告需求的品牌。这样就能把各种资源整合在一起并能撮合成一单生意。

    我再举一个看起来像段子的例子:

    这个故事是关于四川航空的,作者是知乎曹力科,坐飞机的人,去机场或者回家,打车的话,大概平均要花150块。
    然而现实情况是,一没钱,二没车,三没司机。
    但世界上总有些骨骼惊奇的人,能在电光火石间,秀出令人眼花缭乱的操作。
    首先,联系四川航空,跟他们说,如果能做到对乘客免费接送,那么大家坐飞机的意愿会大大增加,这将极大地提升公司的业绩。
    这是显而易见的,谁不愿意坐免费的车呢?但是需要川航出点钱。经过谈判,公司愿意对购买5折以上机票的人,从机票中抽出25元服务费,来换取乘客的免费接送。
    现在,航空公司已经允许了,并且也愿意出25元/人。问题是还没有钱买车,也没有司机。
    其次,联系风行汽车公司,挑了一款14.8万的车,告诉他们,需要购买150辆。当然量这么大,需要卖便宜一点。风行觉得好有道理,于是便宜了一点。又跟他们说,这是机场接送乘客的,可以每接到一个乘客就给他们打一次广告。因为能坐飞机的人还是挺像能买车的人的,嗯,就是传说中的目标客户。既然给你打了广告,那这个车还需要再便宜一点。风行又觉得好有道理。
    最终14.8万的车,风行同意以9万元的价格出售。
    好了,铺垫都已做完,是时候开始真正的表演了。
    发布消息,找来本地需要买车的人。跟他们说,卖给他们一辆车,是川航免费接送乘客的车。每接送一个乘客,川航会支付25元的服务费,当然一辆车一次可以接送好几个乘客。
    这里的客源非常多,这里的竞争非常小,总之,这里的工作非常好。不过,车会卖得比市场价贵一点,17.8万。但相比出租车牌照来说,还是便宜多了。
    司机们很开心,很乐意,甚至还有些激动。因为本来就要买车的,现在不仅有车,还有工作,收入还很不错。为什么不买?买买买!
    仅限报名的前150个司机,先到先得。
    收到司机:150×17.8万=2670万。
    付给风行:150×9万=1350万。
    账上还剩:2670万-1350万=1320万。
    好的,一分钱没花,1320万已经到账。
    这是一个真实的案例,不过我说的时候,做了很多简化。当然具体实施的时候,要考虑很多其他因素,但这里只是说思维。
    这个商业模式有什么好处呢?
    第一,川航多售了机票,当然也增加了利润。
    第二,司机找到了工作,当然也增加了收入。
    第三,风行多卖了汽车,当然还得到了广告。
    最重要的是,乘客坐飞机从此有了免费专车。
    这里面有谁的利益受到伤害了吗?

    2、整合资源

    识别资源之后,整合资源的能力决定了能把资源发挥多大的效用。

    从整合部门资源,到整合公司资源,乃至整合行业资源,能力是依次递升的。

    我举个身边的朋友的例子,这个朋友整合了棋牌行业的上游和下游,搞了个棋牌智囊团,从法律咨询到招人、源码和版号交易,这些服务全部都可以提供。棋牌行业的上游和下游以他为节点,构成了连接。这种人不赚钱简直没有天理。我看了一下他的知识星球,已经有大几十万的收入了。

    而产品经理呢?产品经理更注重逻辑和系统的思考。很难有机会接触和培养资源整合的意识。这是运营和产品极大的不同点。现在还会觉得运营没有前途么?

    3、资源的投入/产出

    总共有两个点:

    第一,要有roi(投资回报率)意识,投入的人力、资源要有尽量可控的回报率;

    第二,需要对资源的投入结合业务深度思考;

    当前的业务投入了哪些资源,产生了哪些效用,中间的流转过程是怎么样的?如果我们在目前某个方向投入了十个人力,能带来十万用户量和一百万的营收,那如果增加投入五十个人力,能否让回报成倍增长呢?中间产生影响的因素和环节有哪些?如果不能成倍增长,我们应该做什么?

    3、顶级阶段:经营价值链

    经营资源的更高一级就是经营价值链。如果我们用传统行业的视角看待互联网产品的价值链,也是类似的。一门生意一般脱离不了供应链、研发、生产、营销、销售、服务等环节。产品、研发部门生产出产品,经过运营部门培育壮大或变成服务,再经过营销部门宣传,最终产生营收。经营价值链就是使这个过程最大限度地实现营收。具体来说,就是低成本获取资源,然后增加附加值,变成可营收的产品或服务,最后高价值卖出去。

    在这里,我认为经营价值链这一步,其本质上是要培养降低交易成本,优化交易结构和提升交易效率的意识。(这就是为什么俞军老师和其他运营大佬都推荐一定要读经济学的书的原因)

    这里可以举例的太多了。最典型的就是微商。

    解释一下什么叫交易成本,如果一件商品的生产成本价是100元,它通过运输、明星代言、一级经销商、二级经销商、商场、门店再到你手里,卖给你1000元,那这900元就是交易成本。

    微商呢?微商其实就是通过朋友圈之间的信任关系,利用天然的信任来降低这个交易成本。这是产品卖到你手里可能只有300元,你当然会觉得便宜而要买买买。

    关于价值链这一层级,我的理解也非常浅薄,还需要持续大量的输入,暂时只能讲这么多。

    综上而言,我认为的运营的边界其实非常宽广,所谓的运营岗位的细分,其实也只是停留在基本层级。如果能跳出这个局限来纵观全局,应该能发现做运营其实非常有意思。

    最后,希望能帮助到所有对运营有怀疑的读者。

  • Redis最佳实践:7个维度+43条使用规范,带你彻底玩转Redis | 附实践清单

    https://cloud.tencent.com/developer/article/1804603

    你的项目或许已经使用 Redis 很长时间了,但在使用过程中,你可能还会或多或少地遇到以下问题:

    • 我的 Redis 内存为什么增长这么快?
    • 为什么我的 Redis 操作延迟变大了?
    • 如何降低 Redis 故障发生的频率?
    • 日常运维 Redis 需要注意什么?
    • 部署 Redis 时,如何做好资源规划?
    • Redis 监控重点要关注哪些指标?

    尤其是当你的项目越来越依赖 Redis 时,这些问题就变得尤为重要。

    此时,你迫切需要一份「最佳实践指南」

    这篇文章,我将从以下七个维度,带你「全面」分析 Redis 的最佳实践优化:

    • 内存
    • 性能
    • 高可靠
    • 日常运维
    • 资源规划
    • 监控
    • 安全