Mac本地快速部署DeepSeek

一、下载Ollama并安装运行DeepSeek 二、下载Chatbox并配置为本地DeepSeek 总体步骤 1.下载Ollama并安装运行DeepSeek 2.下载Chatbox并配置为本地DeepSeek 3.开启DeepSeek本地交互(无需联网) 一、下载Ollama并安装运行DeepSeek 1、从Ollama官方网站下载Ollama: https://ollama.com/ 进入Ollama官方网站后,可以看到Ollama已经支持DeepSeek-R1的部署: 2、点击DeepSeek-R1的链接可以看到有关deepseek-r1的详细介绍: 目前deepseek-r1模型大小提供了7个选择:1.5b、7b、8b、14b、32b、70b、671b。 如果你的Mac内存配置较低(8G内存),建议选择最小的1.5b模型,如果是16G内存,建议选择7B或8B模型,模型选择原则:内存>模型标号 通常模型大小(参数量)越大,模型的理解和生成能力越强,但也会消耗更多的计算资源。至于671B的完全体就不要想了,mac没那么大内存可以跑起来 3、点击Download按钮下载符合自己平台的Ollama: 这里选择macOS,点击下载。 下载文件大小不到200M,文件名为:Ollama-darwin.zip。 4、解压后打开Ollama应用程序,点击next,点击Install 安装ollama。 5、按照提示,打开终端,使用 Command + Space 快捷键调用 terminal: 在终端窗口运行下面命令: 1 ollama run deepseek-r1:8b 6、下载完毕的话,在终端中就直接可以和DeepSeek对话了: 二、下载Chatbox并配置为本地DeepSeek 1、打开Chatbox官方网站:https://chatboxai.app/en 2、下载的Chatbox-1.9.7.dmg,大小100M多点,点击安装,启动: 3、这里不能选择DeepSeek API,若选择这个你就找不到你本地的DeepSeek模型。 正确的应该选择OLLAMA API,然后就可以看到我们上一步安装好的deepseek-r1:8b或其他参数的模型

2025/3/9
articleCard.readMore

Electron的原理

Chromium的原理 Node.js的原理 Electron是一个集成项目,允许开发者使用前端技术开发桌面端应用。它做了如下几个重要的工作: 定制 Chromium,并把定制版本的 Chromium 集成在 Electron 内部; 定制 Node.js,并把定制版本的 Node.js 集成在 Electron 内部; 通过消息轮询机制打通 Node.js 和 Chromium 的消息循环; 通过 Electron 的内置模块向开发者提供桌面应用开发必备的 API; 其中 Chromium 基础能力可以让应用渲染 HTML(CSS) 页面,可以执行页面的JavaScript脚本,让应用可以在 Cookie、LocalStorage或 IndexedDB 中存取数据。除此之外,Electron还允许开发者突破同源策略的限制:伪装请求,截获响应,修改session等。 Node.js 基础能力可以让开发者读写本地磁盘的文件、通过 socket 访问网络、创建和控制子进程等。除此之外,还修改了Node的加解密机制让Chromium的BoringSSL和Node的OpenSSL兼容的更好,让Node.js可以加载asar压缩包内的文件等。 Electron 内置模块可以让开发者创建操作系统的托盘图标、访问操作系统的剪切板、屏幕信息、发送系统通知等,除此之外还提供了崩溃报告收集能力、性能问题追踪能力等。 另外,Electron继承了Chromium的多进程架构,也是分一个主进程多个渲染进程的。 Electron 应用启动时,首先会加载主进程的逻辑,主进程会创建一个或多个窗口,我们暂时可以粗浅的认为一个窗口就代表一个渲染进程,主进程负责管理所有的渲染进程。具体的多进程概念请看Chromium的原理。 Chromium的原理 Chromium是一个多进程架构的浏览器。 以前(大约2008年)浏览器大都是单进程、多线程的架构模式实现的,浏览器中任何一个行为不当的网页或插件都可能让整个浏览器崩溃。 Chromium为了解决这个问题,把每个页面约束在单独的进程中,以保护整个浏览器不受单个页面中的故障所影响。这极大地缓解了浏览器容易崩溃的问题。如下图所示: Chromium把管理页面、管理选项卡和插件的进程称为浏览器进程(Browser Process)。把特定于页面的进程称为渲染进程(Render Process)。 渲染进程使用Blink布局引擎来解释和渲染HTML。渲染进程与浏览器进程通过IPC管道进行通信(详细资料)。 通常每个新窗口或选项卡都会在新进程中打开。浏览器进程(Browser Process)负责创建这些新的进程(Render Process)。一旦渲染进程崩溃或挂起,则浏览器进程控制着界面,提示用户需要重新加载页面,当用户点击重新加载按钮后,浏览器进程则创建一个新的渲染进程来为用户服务。 有的时候创建新窗口或选项卡不会创建新渲染进程。比如开发者使用window.open方法打开新窗口时,就希望这个窗口复用当前的渲染进程,因为两个窗口之间往往需要同步的数据交互。另外还有一些情况需要复用渲染进程,比如打开一个新的渲染进程时,发现系统中已经有一个同样的渲染进程可以复用的情况(详细资料)。 由于渲染进程运行在一个单独的进程中,所有页面脚本都在此进程中运行,当页面脚本尝试访问网络或本地资源时,当前渲染进程会发消息给浏览器进程,由浏览器进程完成相应的工作。此时浏览器进程会判断这些操作是否合法,比如跨越同源策略的请求、突破限制访问Cookie、https页面内嵌http的页面等,这些行为都是不合法的行为,浏览器进程可以拒绝提供服务,这就是浏览器的沙箱模式。 多进程模式还带来了性能上的提升,对于那些不可见的渲染进程,操作系统会在用户可用内存较低时,把它们占用的内存部分或全部交换到磁盘上,以保证用户可见的进程更具响应性。相比之下,单进程浏览器架构将所有页面的数据随机分布在内存中,不可能如此干净地分离使用和未使用的数据,性能表现不佳。 多进程架构模式每个进程都会包含公共基础结构的副本(例如V8引擎的执行环境)、更复杂的通信模型等,这都意味着浏览器会消耗更多的内存、CPU甚至电能。 Node.js的原理 Node.js也是一个集成项目,它允许JavaScript脱离浏览器执行,并提供了一系列的API,供JavaScript访问用户操作系统的资源。 它集成的项目: V8:高性能JavaScript的执行引擎,同时拥有解释执行和编译执行的能力,可以将JavaScript代码编译为底层机器码,Node.js通过V8引擎提供的c++ API使V8引擎解析并执行JavaScript代码,并且通过V8引擎公开的接口和类型把自己内置的C++模块和方法转换为可被JavaScript访问的形式;(Chromium网页中解释执行JS脚本用的也是V8引擎); libuv:高性能、跨平台事件驱动的I/O库,它提供了文件系统、网络、子进程、管道、信号处理、轮询和流的管控机制。它还包括一个线程池,用于某些不易于在操作系统级别完成的异步工作; c-ares:异步DNS解析库。用于支持Node.js的DNS模块; llhttp:一款由TypeScript和C语言编写的轻量级HTTP解析器,内存消耗非常小; OpenSSL:提供了经过严格测试的各种加密解密算法的实现,用于支持Node.js的crypto模块; zlib:提供同步的、异步或流式的压缩和解压缩能力,用于支持Node.js的zlib模块; JavaScript在Node.js运行环境中的执行流程 初始化自己的执行环境:在这个阶段Node.js会注册一系列的C++模块以备将来使用。 创建libuv的消息循环:这个消息循环会伴随着整个应用的生命周期,运行线程退出它才会退出。libuv模块内部持有一个非常复杂的结构体,当用户的代码开始读取文件或发起网络请求时,Node.js就会给这个结构体增加一个回调函数,libuv的消息循环会不断的遍历这个结构体上的回调函数,当读取文件或发起网络请求有数据可用时,就会执行用户的回调函数。 创建V8引擎的运行环境:这是一个拥有自己的堆栈的隔离环境。 绑定底层模块:Node.js会使V8引擎执行一个JavaScript脚本(node_bootstrap.js),这是Node.js内置的一个脚本,这个脚本负责绑定Node.js注册的一系列C++模块。 读取并执行用户脚本的内容:Node.js会把这个文件的内容交给V8引擎运行,并把运行结果返回给用户。

2024/8/28
articleCard.readMore

如何让大语言模型输出JSON格式

提示词明确要求JSON 提示词中给出JSON样例 指定结果开头字符为{ 调整模型参数 重试 提示词明确要求JSON 这是最直接的方法,直接在提示词中提要求,类似这样: 请描述这张图片,输出用JSON格式 就可以让LLM输出JSON格式 当然,这种方法不是100%让结果是JSON,有时候结果会是Markdown,有时候干脆就不是结构化的文本。 提示词中给出JSON样例 给出JSON样例的好处,是可以让LLM在生成的JSON中使用指定的key name。 例如,提示这么写: 描述这张图片,输出为JSON格式,例如{“desc”: “somebody is dancing”, “character_count”: 3} 产生的结果真的就包含desc和character_count两个key。 这是一个业内公认的方法,但是,在实操过程中,我发现对llama 3.2 vision使用这招产生非JSON输出的概率反而更大了,可能因为『描述图片内容』这个任务不容易让LLM上道输出指定的JSON。 指定结果开头字符为{ 前面的方法,有可能结果虽然包含JSON,但是在JSON之前还要加一段废话,为了强制LLM只输出JSON,还有一招,就是在提示词中要求LLM输出以{开头(当然JSON也可以是以[开头),这样更大概率输出的就只有JSON。 上面这些都是在提示词上做文章,除此之外,在模型参数上也可以做一些trick。 调整模型参数 对于OpenAI的API,可以通过调整 logit_bias 来操纵输出token的概率,比如下面的配置,可以增加 { 和 } 字符的输出概率,减少 ''' 的输出概率,从而增大输出为JSON的概率。 1 2 3 4 5 6 7 logit_bias: { "90": 10, // token ID for "{" "92": 10, // token ID for "}" "19317": -10, // token ID for "'''" "19317": -10, // token ID for "'''" "74694": -10 // token ID for "```" } llama 3.2没有对等的logits_bias参数,但是我试了一下调整 temperature 和 top_p 参数,降低temperature和top_p的值,可以让模型少点『创意思维』,老老实实规规矩矩输出,似乎(我只敢说似乎)能够让模型更大概率遵守提示词以JSON格式输出。 重试

2024/8/9
articleCard.readMore

webstorm 的 cpu 占用高

现象 处理 现象 前端项目,不管是 vue,react ,就是 cpu 占用长期 400%以上,有时候持续好多天,导致 macbook 温度一直在 70+度以上 处理 1 2 3 4 /Applications/Webstorm.app/Contents/jbr/Contents/Home/conf/security /Applications/IntelliJ IDEA.app/Contents/jbr/Contents/Home/conf/security /Applications/GoLand.app/Contents/jbr/Contents/Home/conf/security /Applications/RustRover.app/Contents/jbr/Contents/Home/conf/security 修改以上四个文件夹里的 java.security 文件 将 jdk.tls.disabledAlgorithms=SSLv3 替换为 jdk.tls.disabledAlgorithms=TLSv1.3, SSLv3

2024/7/24
articleCard.readMore

修改Joplin主题样式

Joplin主题 macos theme plugin Markdown渲染样式 问题及解决方法 Joplin主题 直接在设置中的“插件”选项中搜索 “macos theme”安装,然后重启后打开macos theme 这个插件,打开该插件对应的设置页面,将样式调成_light_模式,同时Joplin本身的外观设置也需要保持_亮色_模式。再次重启Joplin,发现全局主题已经改变。此外,该插件主题也同时支持暗色主题和自定义主题颜色。 Markdown渲染样式 将源码复制至Joplin配置中的_userstyle.css_中即可,该配置文件可以从_工具_——>_选项_——>_外观_——>_显示高级选项_——>_适用于已渲染Markdown的自定义样式表_中找到。Mac 下路径为 ~/.config/joplin-desktop/userstyle.css 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 pre, code { font-size: 14px; font-family: Roboto, 'Courier New', Consolas, Inconsolata, Courier, monospace; margin: auto 5px; } code { white-space: pre-wrap; border-radius: 2px; display: inline; } pre { font-size: 15px; line-height: 1.4em; display: block; !important; } pre code { white-space: pre; overflow: auto; border-radius: 3px; padding: 1px 1px; display: block !important; } strong, b{ color: #BF360C; } em, i { color: #009688; } hr { border: 1px solid #BF360C; margin: 1.5em auto; } p { margin: 1.5em 5px !important; } table, pre, dl, blockquote, q, ul, ol { margin: 10px 5px; } ul, ol { padding-left: 15px; } li { margin: 10px; } li p { margin: 10px 0 !important; } ul ul, ul ol, ol ul, ol ol { margin: 0; padding-left: 10px; } ul { list-style-type: circle; } dl { padding: 0; } dl dt { font-size: 1em; font-weight: bold; font-style: italic; } dl dd { margin: 0 0 10px; padding: 0 10px; } blockquote, q { border-left: 2px solid #009688; padding: 0 10px; color: #777; quotes: none; margin-left: 1em; } blockquote::before, blockquote::after, q::before, q::after { content: none; } h1, h2, h3, h4, h5, h6 { margin: 20px 0 10px; padding: 0; font-style: bold !important; color: #009688 !important; text-align: center !important; margin: 1.5em 5px !important; padding: 0.5em 1em !important; } h1 { font-size: 24px !important; } h2 { font-size: 20px !important; } h3 { font-size: 18px; } h4 { font-size: 16px; } table { padding: 0; border-collapse: collapse; border-spacing: 0; font-size: 1em; font: inherit; border: 0; margin: 0 auto; } tbody { margin: 0; padding: 0; border: 0; } table tr { border: 0; border-top: 1px solid #CCC; background-color: white; margin: 0; padding: 0; } table tr:nth-child(2n) { background-color: #F8F8F8; } table tr th, table tr td { font-size: 16px; border: 1px solid #CCC; margin: 0; padding: 5px 10px; } table tr th { font-weight: bold; color: #eee; border: 1px solid #009688; background-color: #009688; } 复制完成后保存,重启Joplin即可。 问题及解决方法 当使用_macos theme_的插件主题之后,插件中的主题样式会覆盖部分的_自定义CSS样式_,可以通过在_自定义CSS样式_后添上!important强制使用自定义的样式。 对上文中的 CSS 进行更改: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 pre, code { font-size: 14px !important; font-family: Roboto, 'Courier New', Consolas, Inconsolata, Courier, monospace !important; margin: auto 5px !important; } code { white-space: pre-wrap !important; border-radius: 2px !important; display: inline !important; } pre { font-size: 15px !important; line-height: 1.4em !important; display: block; !important; } pre code { white-space: pre !important; overflow: auto !important; border-radius: 3px !important; padding: 1px 1px !important; display: block !important; } strong, b{ color: #BF360C !important; } em, i { color: #009688 !important; } hr { border: 1px solid #BF360C !important; margin: 1.5em auto !important; } p { margin: 1.5em 5px !important; } table, pre, dl, blockquote, q, ul, ol { margin: 10px 5px !important; } ul, ol { padding-left: 15px !important; } li { margin: 10px !important; } li p { margin: 10px 0 !important; } ul ul, ul ol, ol ul, ol ol { margin: 0 !important; padding-left: 10px !important; } ul { list-style-type: circle !important; } dl { padding: 0 !important; } dl dt { font-size: 1em !important; font-weight: bold !important; font-style: italic !important; } dl dd { margin: 0 0 10px !important; padding: 0 10px !important; } blockquote, q { border-left: 2px solid #009688 !important; padding: 0 10px !important; color: #777 !important; quotes: none !important; margin-left: 1em !important; } blockquote::before, blockquote::after, q::before, q::after { content: none !important; } h1, h2, h3, h4, h5, h6 { margin: 20px 0 10px !important; padding: 0 !important; font-style: bold !important; color: #009688 !important; text-align: center !important; margin: 1.5em 5px !important; padding: 0.5em 1em !important; } h1 { font-size: 24px !important; border-bottom: 1px solid #ddd !important; } h2 { font-size: 20px !important; border-bottom: 1px solid #eee !important; } h3 { font-size: 18px !important; } h4 { font-size: 16px !important; } table { padding: 0 !important; border-collapse: collapse !important; border-spacing: 0 !important; font-size: 1em !important; font: inherit !important; border: 0 !important; margin: 0 auto !important; } tbody { margin: 0 !important; padding: 0 !important; border: 0 !important; } table tr { border: 0 !important; border-top: 1px solid #CCC !important; background-color: white !important; margin: 0 !important; padding: 0 !important; } table tr:nth-child(2n) { background-color: #F8F8F8 !important; } table tr th, table tr td { font-size: 16px !important; border: 1px solid #CCC !important; margin: 0 !important; padding: 5px 10px !important; } table tr th { font-weight: bold !important; color: #eee !important; border: 1px solid #009688 !important; background-color: #009688 !important; } 更改后发现自定义的CSS样式不再会被插件主题样式覆盖。

2024/7/21
articleCard.readMore

ECMAScript 历代版本

欧洲计算机制造商协会(European Computer Manufacturers Association)或简称 ECMA. ECMA走向全球后,在1994 年更名为:Ecma International。其商标“ECMA”由于历史原因被保留了下来,沿用至今。 ECMA-262是一种规范(官网中可以查到各个版本的PDF提案),自然会有满足这种规范的具体的语言被创造出来。最著名的就是JavaScript (现在商标属于Oracle),在1995年由Netscape公司的Brendan Eich,在Netscape导航者浏览器上首次设计实现而成。因为Netscape与Sun合作,Netscape管理层希望它外观看起来像Java,因此取名为JavaScript。但实际上它的语法风格与Self及Scheme较为接近。其设计思路如下: 借鉴 C 语言的基本语法。 借鉴 Java 语言的数据类型和内存管理。 借鉴 Scheme 语言,将函数提升到“第一等公民”(first class)的地位。 借鉴 Self 语言,使用基于原型(prototype)的继承机制。 ECMAScript 各版本 ECMAScript 1.0 (1997):第一个正式标准,规范化了 JavaScript 的核心语言。 ECMAScript 3.0 (1999):该版本为语言带来了正则表达式、try/catch 异常处理以及更多的核心改进。 ECMAScript 5.0 (2009):引入了 strict mode(严格模式),并添加了许多数组方法(如 forEach、map、filter 等)。这次更新被认为是 JavaScript 现代化的重要一步。 ECMAScript 6 (ES2015):这是 ECMAScript 发展中的里程碑式版本,增加了 let 和 const 变量声明、类和模块系统、箭头函数、解构赋值、模板字符串、Promise、Symbol、迭代器和生成器等新特性。 ECMAScript 7 (ES2016):主要是增加了两个新特性:Array.prototype.include和取幂运算符。 ECMAScript 8 (ES2017):主要变化包括:Object.values/Object.entries、字符串填充、Object.getOwnPropertyDescriptor、尾随逗号、异步函数、共享内存和原子,等。 ECMAScript 9 (ES2018):ECMAScript 2018 主要包含内容: 异步迭代器:原生支持在 JavaScript 中对异步获取的数据做迭代。 Object Rest/Spread Properties Promise.prototype.finally Template Literal(模板字面量):取消 Escape-Sequenzen 限制 正则表达式: 支持 s (dotAll) 模式 Unicode 属性转义(Property Escape) 支持后行断言(Lookbehind Assertions) 命名捕获组(named capture group) ECMAScript 10 (ES2019): Array.prototype.flat() 和 Array.prototype.flatMap() Object.fromEntries() String.prototype.trimStart() 和 String.prototype.trimEnd() Array.prototype.sort() 改进 Symbol.prototype.description Function.prototype.toString() 的改进 JSON.prototype.{stringify, parse} 的改进 JSON.stringify() 现在会忽略 undefined、symbol、函数 等值。 JSON.parse() 对 NaN、Infinity 和 -Infinity 的处理变得更加严格,报错而不是返回这些值。 RegExp.prototype.flags. RegExp.prototype.flags 属性返回正则表达式的标志(如 g, i, m 等),这是一个只读的属性,简化了获取正则标志的方式。 Optional Catch Binding (可选的 catch 绑定). 在 ES10 中,try…catch 语句中的 catch 变量现在是可选的,可以不指定错误对象变量。 Well-formed JSON.stringify() 的改进. ES10 对 JSON.stringify() 进行了改进,确保序列化结果是符合 JSON 格式的,修复了一些潜在的问题。 ECMAScript 11 (ES2020): Optional Chaining(可选链操作符 ?.) Nullish Coalescing Operator(空值合并操作符 ??) BigInt Promise.allSettled() globalThis String.prototype.matchAll() Array.prototype.sort() 的稳定性 模块化的顶层 await Import.meta 模块内的顶层 await 和新的 import 机制

2024/6/20
articleCard.readMore

新一代包管理器 PNPM

依赖管理的始末 npm2 npm3 结论 PNPM 依赖安装 依赖管理原理 PNPM 机制 基于软链接的 node_modules PNPM 锁文件 总结 依赖管理的始末 npm2 使用早期的 npm1/2 安装依赖,node_modules 文件夹会以递归的形式呈现,严格按照 package.json 结构以及次级依赖的 package.json 结构将依赖安装到它们各自的 node_modules 中,直到次级依赖不再依赖其它模块。 就像下面这样,tea-app 依赖 tea-component 作为次级依赖,tea-component 会安装到 tea-component 的 node_modules 里面: 1 2 3 4 5 6 7 8 node_modules └─ tea-app ├─ index.js ├─ package.json └─ node_modules └─ tea-component ├─ index.js └─ package.json 假设项目的中的两个依赖同时依赖了相同的次级依赖,那么它们二者的次级依赖将会被重复安装: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 node_modules ├─ tea-app │ ├─ index.js │ ├─ package.json │ └─ node_modules │ └─ tea-component │ ├─ index.js │ └─ package.json └─ tea-chart ├─ index.js ├─ package.json └─ node_modules └─ tea-component ├─ index.js └─ package.json 这只是简单的例子,那如果 tea-component 还依赖别的包,别的包又依赖另外的包…… 在真实的开发场景中其问题还会更加恶劣: 依赖层级太深,会导致文件路径过长 重复的包被安装,导致 node_modules 文件体积巨大,占用过多的磁盘空间 npm3 自 npm3/yarn 开始,相比 npm1/2 项目依赖管理的方式有了很大的改变,不再是以往的“嵌套式”而是采用了“扁平化”方式去管理项目依赖。 这里继续拿上面的例子,tea-app 和 tea-chart 都依赖了 tea-component,依赖安装后呈现的是下面的这种扁平化目录: 1 2 3 4 5 6 7 8 9 10 node_modules ├─ tea-component │ ├─ index.js │ └─ package.json ├─ tea-app │ ├─ index.js │ └─ package.json └─ tea-chart ├─ index.js └─ package.json 扁平化的目录的确解决了上一小节暴露的一些问题,同时也暴露了新的问题: Phantom dependencies 称为幽灵依赖,指的是在项目内引用未在 package.json 中定义的包。这个问题在 npm3 展现,因为早期的树形结构导致了依赖冗余和路径过深的问题,npm3 之后采用扁平化的结构,一些第三方包的次级依赖提升到了与第三方包同级。 一旦出现幽灵依赖的问题,可能会导致意想不到的错误,所以一定要正视: 不兼容的版本(例如某一个 api 进行了重大更新) 有可能会丢失依赖(某依赖不再依赖呈现在我们项目中的幽灵依赖) 1 2 // tea-component 就属于是幽灵依赖,因为它是属于 tea-app、tea-chart 的次级依赖。 import { Button } from 'tea-component'; NPM doppelgangers 称为分身依赖依赖的同名包都会被重复安装。 在实际开发中也会出现这样的情景,假设 tea-app、tea-form 依赖 tea-component@2.0.0,tea-chart 依赖 tea-component@3.0.0,这时候会造成依赖冲突,解决冲突的方式会将对应的冲突包放到对应依赖目录的 node_mudules 中,类似下面结构: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 node_modules ├─ tea-component@3.0.0 │ ├─ index.js │ └─ package.json ├─ tea-app │ ├─ index.js │ ├─ package.json │ └─ node_modules │ └─ tea-component@2.0.0 │ ├─ index.js │ └─ package.json ├─ tea-form │ ├─ index.js │ ├─ package.json │ └─ node_modules │ └─ tea-component@2.0.0 │ ├─ index.js │ └─ package.json └─ tea-chart ├─ index.js └─ package.json 这时候会发现一个问题,tea-app、tea-form 的 node_modules 下都有重复且版本相同的 tea-component@2.0.0,这个问题就是我们正在所说的“分身依赖”的问题。这个问题就会导致 tea-app 中的 ConfigProvider 组件和 tea-form 的不是一个实例,无法生效。 常见的问题: 项目打包会将这些“重身”的依赖都进行打包,增加产物体积 无法共享库实例,引用的得到的是两个独立的实例 重复 TypeScript 类型,可能会造成类型冲突 结论 扁平化的 node_modules 结构允许访问没有在 package.json 中声明的依赖。 安装效率低,大量依赖被重复安装,磁盘空间占用高。 多个项目之间已经安装过的的包不能共享,每次都是重新安装。 PNPM Fast, disk space efficient package manager (速度快、节省磁盘空间的软件包管理器) 当使用 npm 或 Yarn 时,如果你有 100 个项目,并且所有项目都有一个相同的依赖包,那么, 你在硬盘上就需要保存 100 份该相同依赖包的副本。然而,如果是使用 pnpm,依赖包将被 存放在一个统一的位置,因此: 如果你对同一依赖包需要使用不同的版本,则仅有 版本之间不同的文件会被存储起来。例如,如果某个依赖包包含 100 个文件,其发布了一个新 版本,并且新版本中只有一个文件有修改,则pnpm update只需要添加一个 新文件到存储中,而不会因为一个文件的修改而保存依赖包的 所有文件。 所有文件都保存在硬盘上的统一的位置。当安装软件包时, 其包含的所有文件都会硬链接自此位置,而不会占用 额外的硬盘空间。这让你可以在项目之间方便地共享相同版本的 依赖包。 最终结果就是以项目和依赖包的比例来看,你节省了大量的硬盘空间, 并且安装速度也大大提高了! 依赖安装 使用 pnpm 安装,pnpm 会将依赖存储在位于 .pnpm-store 目录下。只要你在同一机器下,下次安装依赖的时候 pnpm 会先检查 store 目录,如果有你需要的依赖则会通过一个硬链接到你的项目中去,而不是重新安装依赖。 依赖管理原理 pnpm 会将依赖存储在 store 目录下,通过符号链接的方式仅将项目的直接依赖项添加到 node_modules 的根目录下。 当使用 npm 或 yarn 安装依赖包时,所有软件包都将被提升到 node_modules 的 根目录下。其结果是,源码可以访问 本不属于当前项目所设定的依赖包。 PNPM 机制 如果 store 目录里面拥有即将需要下载的依赖,下载将会跳过,会向对应项目 node_modules 中去建立硬链接,并非去重新安装它。这里就表明为什么 pnpm 性能这么突出了,最大程度节省了时间消耗和磁盘空间。 基于软链接的 node_modules pnpm 输出的 node_modules 与 npm/yarn 有很大的出入,并非是先者那样的“扁平化目录”而是“非扁平化目录”。 创建两个目录并分别运行 npm add express,pnpm add express。 这是使用 npm 安装 node_modules 的结构: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 .bin accepts array-flatten body-parser bytes content-disposition cookie-signature cookie debug depd destroy ee-first encodeurl escape-html etag express 这个则是 pnpm 安装 node_modules 的结构: 1 2 3 .pnpm .modules.yaml express 打开 .pnpm 目录会发现这些依赖都被“扁平化”了,每个包都携带着自己的版本号。pnpm 这样设计的目的我理解其实是为了解决“分身依赖”的问题。 假设我们有这么一个情景,项目中依赖了 tea-app@1.0.0、tea-chart@1.0.0 和 tea-component@2.0.0。tea-chart 和 tea-app 依赖了 tea-component@1.0.0 那它引用关系是这样的: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 node_modules ├─tea-app -> ./.pnpm/tea-app@1.0.0/node_modules/tea-app ├─tea-chart -> ./.pnpm/tea-chart@1.0.0/node_modules/tea-chart ├─tea-component -> ./.pnpm/tea-component@2.0.0/node_modules/tea-component └─.pnpm ├─ tea-app@1.0.0 │ └─ node_modules │ ├─ tea-component -> ../tea-component@1.0.0/node_modules/tea-component │ └─ tea-app -> <store>/tea-app ├─ tea-chart@1.0.0 │ └─ node_modules │ ├─ tea-component -> ../tea-component@1.0.0/node_modules/tea-component │ └─ tea-chart -> <store>/tea-chart ├─ tea-component@1.0.0 │ └─ node_modules │ └─ tea-component -> <store>/tea-component@1.0.0 └─ tea-component@2.0.0 └─ node_modules └─ tea-component -> <store>/tea-component@2.0.0 为什么需要通过软链接的方式去引用实际的依赖? 这样设计的目的是解决“幽灵依赖”的问题,只有声明过的依赖才会以软链接的形式出现在 node_modules 目录中。在实际项目中引用的是软链接,软链接指向的是 .pnpm 的真实依赖,所以在日常开发中不会引用到未在 package.json 声明的包。 PNPM 锁文件 pnpm 产出的是一个 pnpm-lock.yaml 格式的锁文件。 支持通过 pnpm import 从另一个包管理器的锁文件生成一个。支持的源文件: package-lock.json npm-shrinkwrap.json yarn.lock 总结 npm2 是通过嵌套的方式管理 node_modules 的,会有同样的依赖复制多次的问题。 npm3+ 和 yarn 是通过铺平的扁平化的方式来管理 node_modules,解决了嵌套方式的部分问题,但是引入了幽灵依赖的问题,并且同名的包只会提升一个版本的,其余的版本依然会复制多次。 pnpm 则是用了另一种方式,不再是复制了,而是都从全局 store 硬连接到 node_modules/.pnpm,然后之间通过软链接来组织依赖关系。 这样不但节省磁盘空间,也没有幽灵依赖问题,安装速度还快,从机制上来说完胜 npm 和 yarn。

2024/6/16
articleCard.readMore

React useEffect() Hook

Side Effect 是什么 Hook 的作用 useEffect() 的用法 useEffect() 的第二个参数 useEffect() 的用途 useEffect() 的返回值 Side Effect 是什么 函数式编程将那些跟数据计算无关的操作,都称为 side effect 。如果函数内部直接包含产生 side effect 的操作,就不再是纯函数了. 纯函数内部只有通过间接的手段(即通过其他函数调用),才能包含 side effect Hook 的作用 hook 就是 React 函数组件的副效应解决方案,用来为函数组件引入副效应。 函数组件的主体只应该用来返回组件的 HTML 代码,所有的其他操作(副效应)都必须通过钩子引入。 由于副效应非常多,所以钩子有许多种。React 为许多常见的操作(副效应),都提供了专用的钩子。 useState():保存状态 useContext():保存上下文 useRef():保存引用 …… 上面这些钩子,都是引入某种特定的副效应,而 useEffect() 是通用的副效应钩子 。找不到对应的钩子时,就可以用它。 useEffect() 的用法 例如,希望组件加载以后,网页标题(document.title)会随之改变。那么,改变网页标题这个操作,就是组件的副效应,必须通过 useEffect() 来实现。 1 2 3 4 5 6 7 8 import React, { useEffect } from 'react'; function Welcome(props) { useEffect(() => { document.title = '加载完成'; }); return <h1>Hello, {props.name}</h1>; } useEffect()的参数是一个函数,它就是所要完成的副效应(改变网页标题)。组件加载以后,React 就会执行这个函数。 useEffect() 的第二个参数 如果不希望 useEffect() 每次渲染都执行,可以使用它的第二个参数,使用一个数组指定副效应函数的依赖项,只有依赖项发生变化,才会重新渲染。 1 2 3 4 5 6 function Welcome(props) { useEffect(() => { document.title = `Hello, ${props.name}`; }, [props.name]); return <h1>Hello, {props.name}</h1>; } 上面例子中,useEffect()的第二个参数是一个数组,指定了第一个参数(副效应函数)的依赖项(props.name)。只有该变量发生变化时,副效应函数才会执行。 如果第二个参数是一个空数组,就表明副效应参数没有任何依赖项。因此,副效应函数这时只会在组件加载进入 DOM 后执行一次,后面组件重新渲染,就不会再次执行。 useEffect() 的用途 只要是副效应,都可以使用useEffect()引入。它的常见用途有下面几种。 获取数据(data fetching) 事件监听或订阅(setting up a subscription) 改变 DOM(changing the DOM) 输出日志(logging) 下面是从远程服务器获取数据的例子。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import React, { useState, useEffect } from 'react'; import axios from 'axios'; function App() { const [data, setData] = useState({ hits: [] }); useEffect(() => { const fetchData = async () => { const result = await axios( 'https://hn.algolia.com/api/v1/search?query=redux', ); setData(result.data); }; fetchData(); }, []); return ( <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> ); } export default App; 上面例子中,useState()用来生成一个状态变量(data),保存获取的数据;useEffect()的副效应函数内部有一个 async 函数,用来从服务器异步获取数据。拿到数据以后,再用setData()触发组件的重新渲染。 由于获取数据只需要执行一次,所以上例的useEffect()的第二个参数为一个空数组。 useEffect() 的返回值 副效应是随着组件加载而发生的,那么组件卸载时,可能需要清理这些副效应。 useEffect()允许返回一个函数,在组件卸载时,执行该函数,清理副效应。如果不需要清理副效应,useEffect()就不用返回任何值。 1 2 3 4 5 6 useEffect(() => { const subscription = props.source.subscribe(); return () => { subscription.unsubscribe(); }; }, [props.source]); 上面例子中,useEffect()在组件加载时订阅了一个事件,并且返回一个清理函数,在组件卸载时取消订阅。 实际使用中,由于副效应函数默认是每次渲染都会执行,所以清理函数不仅会在组件卸载时执行一次,每次副效应函数重新执行之前,也会执行一次,用来清理上一次渲染的副效应。

2024/5/18
articleCard.readMore

Vue 2.x 使用高德地图JS API 2.0加载起点终点路径轨迹

需求 在 html 文件中引入地图 js 主页面 地图组件 代码解析 调起地图H5 组件 需求 在地图中显示行驶轨迹,自定义标记点图标 地图厂商使用高德地图,使用目前最新的高德地图JSAPI 2.0 在自己的 H5 中调起多个地图app,显示标记点位置 由于地图 APP 并不支持在自己的网页中直接打开,因此需要通过地图 URI API 调用厂商H5地图,在厂商H5地图调起地图app 在 html 文件中引入地图 js 1 <script src="https://webapi.amap.com/maps?v=2.0&key=申请的key值&plugin=AMap.Driving"></script> 主页面 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 // index.vue <template> <div class="index"> <template > <v-map :data="dataList"></v-map> <v-detail :statrLocation="statrLocation" :endLocation="endLocation"/> </template> </div> </template> <script> import Map from './mMap.vue' // 地图 import Detail from './detail.vue' export default { name: 'index', data() { return { dataList: [], statrLocation: {}, endLocation: {}, } }, components: { "v-map": Map, "v-detail": Detail }, mounted() { this.dataList = [ { longitude: 116.478346, latitude: 39.997361 }, { longitude: 116.402796, latitude: 39.936915 } ] this.statrLocation = this.dataList[0] this.endLocation = this.dataList[this.dataList.length-1] }, methods: { } }; </script> <style scoped lang="scss"> .index { position: relative; width: 100%; height: 100%; background: #fcf9f2; } </style> 地图组件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 // mMap.vue <template> <div class="m-map"> <div id="map-box"></div> </div> </template> <script> export default { props: ['data'], data() { return { map: {}, lineArr: [], }; }, created() { this.initMap() }, methods: { initMap() { this.$nextTick(() => { this.map = new AMap.Map('map-box', { resizeEnable: true, //是否监控地图容器尺寸变化 zoom: 14, //初始化地图层级 center: [116.397428, 39.90923], //初始化地图中心点 animateEnable: true// 地图平移过程中是否使用动画 }); if(this.lineArr.length) { this.drawLine() //绘制路线 } }); }, drawLine(){ AMap.convertFrom(this.lineArr, 'gps', (status, result) => { if (result.info === 'ok') { const paths = result.locations; this.map.clearMap() this.startMarker = new AMap.Marker({ map: this.map, position: paths[0], //起点经纬度 icon: new AMap.Icon({ image: require('@/assets/img/icon/icon-start.png'), size: new AMap.Size(120, 120), //图标所处区域大小 imageSize: new AMap.Size(120,120) //图标大小 }), //起点ico offset: new AMap.Pixel(-60, -60), autoRotation: true, // angle:-90, }); this.endMarker = new AMap.Marker({ map: this.map, position: paths[paths.length-1], //终点经纬度 icon: new AMap.Icon({ image: require('@/assets/img/icon/icon-end.png'), size: new AMap.Size(60, 60), //图标所处区域大小 imageSize: new AMap.Size(60,60) //图标大小 }), //终点ico offset: new AMap.Pixel(-30, -30), autoRotation: true, }); // 绘制轨迹 var polyline = new AMap.Polyline({ map: this.map, path: paths, showDir: true, strokeColor: '#28F', //线颜色 // strokeOpacity: 1, //线透明度 strokeWeight: 6, //线宽 // strokeStyle: "solid" //线样式 }); this.map.add([this.startMarker, this.endMarker]); this.map.setFitView(); //自适应缩放级别 } }) } }, watch: { data: { handler(newValue, oldValue) { this.lineArr = []; if(newValue.length) { newValue.map((item, index) => { if( item.longitude != null && item.latitude != null ) { this.lineArr.push(new AMap.LngLat(item.longitude,item.latitude)); } }); this.drawLine(); } }, immediate: true }, }, }; </script> <style scoped lang ="scss"> @import '@/assets/scss/mixin.scss'; .m-map { width: 100%; height: 100vh; #map-box { width: 100%; height: 100%; } } </style> 代码解析 在使用中发现标记点位置显示不对,存在一定的偏移,需要将其他坐标转为高德坐标方法. 在绘制轨迹之前先转换为高德坐标,然后再删除地图上所有的覆盖物, 1 2 3 4 5 6 7 8 9 AMap.convertFrom(this.lineArr, 'gps', (status, result) => { if (result.info === 'ok') { const paths = result.locations; this.map.clearMap() // ... } }) 也可以使用专门处理地理坐标系的js库gcoord,用来修正百度地图、高德地图及其它互联网地图坐标系不统一的问题。 调起地图H5 组件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 // detail.vue <template> <div class="m-detail-box"> <div class="m-btn-wrapper"> <van-button class="open-btn" round @click="openShow = true">导航至车辆当前位置</van-button> </div> <van-action-sheet class="m-sheet-box" v-model="openShow" :actions="actions" cancel-text="取消" close-on-click-action @select="onSelect" /> </div> </template> <script> import { Toast } from 'vant'; export default { props: { statrLocation: { type: Object, default: () => ({}) }, endLocation: { type: Object, default: () => ({}) }, }, data() { return { openShow: false, actions: [ { name: '使用苹果地图导航', value: 'iosamap', color: '#007AFF' }, { name: '使用百度地图导航', value: 'bmap', color: '#007AFF' }, { name: '使用高德地图导航', value: 'amap', color: '#007AFF' } ], } }, filters: { }, mounted() { }, methods: { onSelect(item) { this.openShow = false; let startLocation = this.startLocation let endLocation = this.endLocation if (endLocation.longitude && endLocation.latitude) { let url = '' switch (item.value) { case 'iosamap': url = `iosamap://navi?sourceApplication=applicationName&backScheme=applicationScheme&poiname=${location}&poiid=BGVIS&lat=${endLocation.latitude}&lon=${endLocation.longitude}&dev=1&style=2` break; case 'bmap': // 单点标注 url = `http://api.map.baidu.com/marker?location=${endLocation.latitude},${endLocation.longitude}&title=车辆位置&content=实时定位&output=html&coord_type=wgs84&src=webapp.baidu.openAPIdemo` // 路径规划 // url = `http://api.map.baidu.com/direction?origin=latlng:${startLocation.latitude},${startLocation.longitude}|name:我的位置&destination=latlng:${endLocation.latitude},${endLocation.longitude}|name:实时定位&mode=driving&coord_type=wgs84&src=webapp.baidu.openAPIdemo` break; case 'amap': // 单点标注 url = `https://uri.amap.com/marker?position=${endLocation.longitude},${endLocation.latitude}&name=实时定位&src=mypage&coordinate=wgs84&callnative=1` // 路径规划 // url = `https://uri.amap.com/navigation?from=${startLocation.longitude},${startLocation.latitude},我的位置&to=${endLocation.longitude},${endLocation.latitude},实时定位&mode=car&policy=1&coordinate=wgs84&callnative=1` break; } window.open(url) } else { Toast({ message: '暂无车辆定位', type: 'fail', }) } }, }, beforeDestroy() { clearInterval(this.timer) } } </script> <style scoped lang="scss"> @import "@/assets/scss/mixin.scss"; .m-detail-box { position: absolute; bottom: 0; padding: 20px 0 0; border-radius: 20px 20px 0px 0px; width: 100%; background: #FEFFFE; box-shadow: 0 4px 40px 4px rgba(135, 119, 145, 0.36); z-index: 160; .van-cell-group { &::after { border: none; } } .van-cell { padding: 12px 24px; font-size: 16px; font-weight: 600; &::after { left: 24px; right: 24px; } .van-cell__title { flex: none; color: #757AB5; } .van-cell__value { color: #292929; } } .m-btn-wrapper { border-top: 1px solid #EFF2F9; background: #FFF; .open-btn { display: block; margin: 10px auto; padding: 14px; width: 90%; font-size: 18px; color: #FEFFFE; background: #85D4D9; } } /deep/.van-overlay { background: rgba(33, 34, 51, 0.5); } .m-sheet-box { padding: 0 8px; background: transparent; .van-action-sheet__content { border-radius: 14px; background: rgba(255, 255, 255, 0.92); } .van-action-sheet__gap { height: 20px; background: transparent; } .van-action-sheet__cancel { margin-bottom: 20px; border-radius: 14px; font-size: 20px; color: #007AFF; background: rgba(255, 255, 255, 0.92); } } } </style>

2024/5/1
articleCard.readMore

家庭用电插座

10a和16a插座的区别 1、外观区别 2、使用区别 3、插座金属 4、承受范围 哪些电器用16a插座 10a和16a插座的区别 1、外观区别 主要是插孔间距的尺寸区别。16a插座三插孔的孔距比10a插座更大。所以不同标准的插孔和插头并不能适用。 10a插座为五眼插:1个三眼、1个二眼,而16a插座是一个三眼插,比10a三眼插宽广些。 2、使用区别 16a插头和10a插头不通用,10a插头插不到16a插座里去,当然反过来也是不行的。 3、插座金属 16a插座承载电流大于10a插座,用到的铜也比较多,而10a插座用的材料也有所不同。 16a插座需要布置4平方毫米以上规格的铜线,而10a插座最好布置2.5平方毫米铜线。 4、承受范围 16a插座可以承受3000瓦以内电器功率,而10a插座功率控制在2200瓦以内,不然容易发生意外。 哪些电器用16a插座 家里常用的大功率电器主要是空调、电磁炉、热水器 16a插座是承受范围比较大的插座,家居中一般主要在空调电器上使用,所以我们经常可以见到空调的隔壁安装的是一个不一样的插座,这是对用电安全的需求

2024/4/11
articleCard.readMore

MacOS 14.4 引发Java 应用崩溃

根据Java官方发布的文章,由于macOS上运行的进程可能会访问受保护内存区域中的内存。在 macOS 14.4 更新之前,在某些情况下,macOS 内核会通过向进程发送信号 SIGBUS 或 SIGSEGV 来响应这些受保护的内存访问。然后该进程可以选择处理该信号并继续执行。而在最新 macOS 14.4 中,当线程在写入模式下运行时,如果尝试对受保护的内存区域进行内存访问,macOS 将发送信号SIGKILL。该进程无法处理该信号,并且该进程将无条件终止。 目前该问题主要受影响的Mac机型和Java版本包括: Mac机型:M1、M2、M3(Apple Silicon m* 芯片) Java版本:Java 8 - Java 22 所有版本 如果还在使用Intel芯片的话,这次不受影响。 官方文章 Java users on macOS 14 running on Apple silicon systems should skip macOS 14.4 and update directly to macOS 14.4.1 (oracle.com) 在x上,Java开发领域的一些大v们,也发现了这个问题,并提醒大家不要升级。 其他资料 Java users on macOS 14 running on Apple silicon systems should consider delaying the macOS 14.4 update | Lobsters

2024/3/15
articleCard.readMore

重定向广告

为什么在京东搜索的商品,会展示在抖音广告上 不知道你是不是也会遇到这个情况,你刚才说想要个戴森吹风机,头条APP里的广告就展示了戴森的广告。 当你在京东的搜索框里搜索了蒸锅后,你的其他软件可能就被蒸锅攻陷了。 头条广告里看到苏泊尔蒸锅的广告,刷抖音时能看到苏泊尔的广告,上个知乎还能看到苏泊尔的广告……。是不是感觉自己完全陷入了恶性循环,进了套路里。 甚至是,你从两部手机切换过后,还是会看到苏泊尔蒸锅的广告! 如果你认为是巧合,那就大错特错了,其实这是广告界常用的产品,叫做重定向广告。 这个操作对广告主来说那是极好的,实际上是却给人带来了极大的恐惧感。就好比你在微信的聊天记录,公开到了所有产品上,被所有人无情鞭策。 互联网时代信息公开透明,但却让人们少了隐私。 为什么主流产品都这么干 思考一个场景: 假设你在京东搜索电视机,可能你是有购买意向的,但最后没有下单转化。对于平台来说,损失了一个客户。如果,同时有几万人都有这个行为,那对平台来说就损失很大了。 所以,平台要做两件事。 一是在站内,做二次营销。 比如弹出搜索的专属红包,或专属优惠券,用红包权益进行二次刺激。 如下图每日优鲜的截图,搜索之后,进入商详页,会直接给相关的优惠券,刺激下单转化。据说让利10元,可以提升转化率40%以上。 二是在站外,做重定向广告。 它就像定位器一样,无时不刻找到你并给你展示广告。假如你离开了网站,平台就会有这个方法牢牢拴住你。 比如在京东搜索蒸锅,马上在头条就会投放蒸锅的广告,而且每次的广告品牌可能也会不一样 据 Google Adwords提供的数据,在30天内为同一个用户展示7~10次广告的转化效果最好,做到这个程度的广告收益可以达到三倍以上。 站内浏览广告并购买的用户仅为5%,有95%的用户流失掉了,拉回并转化这些用户,则是重定向广告的使命。 坦福大学商学院的营销学教授Navdeep Sahni曾经做过一个实验,他利用各种重定向活动对 http://BuildDirect.com 的23w用户进行访问观测。 在观测期间,有的用户零广告观看,有的用户观看15次以上,经过4周的试验后,得出结论:重定向广告增加了他们返回网站的可能性接近15%,很大一部分人因为重定向广告,改变了行为。 整个过程是什么样子的 其实,整个过程并不复杂 假如你作为电商网站的产品经理,如果想要接入重定向广告能力,需要做这几步, 1. 在你的网站埋入统计追踪代码 有些是广告联盟提供的是插件或者SDK,比如 Avazu DSP提供的就是一个插件,给到其他商家店铺。 这份代码主要目的是为了把用户的信息记录到浏览器或手机本地的cookie里,其投放平台读取这份cookie,其中包含用户id,电话号,基本信息,访问信息等。 当然,cookie不是万能的,还会引入其他的手段继续识别用户身份,比如设备识别码,手机信息,web浏览器、操作系统、屏幕分辨率、时区、语言、插件、字体等 这些信息可以确定唯一的用户身份,当你换设备的时候,其实已经通过这些信息再一次进行了关联。 逃?那是不可能的,细思极恐吧! 2. 在线竞价 先普及一下广告业务的竞价排名。简单来讲,就是通过价格优势来竞争广告位。 比如百度的广告,你出价“起点学院”的广告词,虽然你是起点学院的负责人老曹,但是并不一定能够拿到这个词,因为友商三节课花的价钱更高,拿到了“起点学院”这个词。 当用户在百度搜索“起点学院”时,出现的是三节课的推广(当然,这并不会发生)。 我们回到重定向广告的竞价,其实一个道理,用户在淘宝搜索蒸锅,也在京东搜索的蒸锅,那到底是展示淘宝的推广还是京东的推广呢? 还是一决雌雄吧,我们靠竞价说话,谁价格高,广告联盟就出谁的广告。 3. 展示广告 凡是和广告联盟对接的流量平台, 都可以承接广告。当流量平台识别出用户id后,自动替换掉默认出的广告图,打上个性化广告。 下面的是Avazu DSP可以投放的流量平台,用户在这些流量渠道即可看到重定向广告。 当重定向发展到第二阶段时,由原来的搜索1对1关系,扩散到1对多的关系。算法的引入,可以更精准预测了用户的购买需求。 比如本来搜索的是大疆无人机,那么推荐一些和大疆相关的其他商品,增加购买的可能性。 细思极恐的东西 作为一个吃瓜群众,莫名其妙的就被广告主割了韭菜,防不胜防,也无法防。 在2017年,苹果推出了新功能叫智能反追踪,并集成在 Safari浏览器中,为了保护用户的隐私不被泄露。 不过,苹果受不住利益的诱惑,在可以保住底裤的同时,今年又推出了隐私保护广告点击归因,其旨在即能保护用户隐私,还能给广告主信息进行广告投放。 简单来说,是用户把自己的信息存储到了cookie中,或者存储到手机设备中,广告主可以识别这些信息对应到人,但是却无法解析到更多其他信息了。 这个做法和阿里的数据银行比较相似,用户的详细数据是明确禁止外部传播的,数据银行给品牌产出的数据仅仅是人群包,人群包只能在内部有权限的业务识别,保障了数据的安全性。 微信刚聊完就收到商品推荐,电商App在监视我吗 当你搜索、点击、浏览、收藏、购买了某件商品后,紧接着就会收到网站或电商平台的相关广告推送,这已经不是什么新鲜事了,你在互联网上的一举一动,在商家眼里就是大数据和用户画像。 但是,当你的微信聊天记录、和同事面对面说话时的聊天内容、手机相册里的照片也会被电商APP用于推荐广告时,你会感到害怕吗? 近日有用户称,自己在微信群聊中讨论过一款雨伞,随后就收到了电商平台的短信推送。不仅如此,不少用户反映自己的手机相册、面对面聊天中的内容都有可能已被“窃听”,因为收到了与此相关的精准广告推送。 事实上,一些应用软件在安装时就获取了用户位置、相机、麦克风等诸多权限,你的所有信息和随后的浏览、搜索行为都会成为一个一个的数据库文件,最终组成有标签、有画像的“另一个自己”。不光地域、性别、消费习惯,商家还能知道你手机里装了哪些APP。 从技术上来看,分析提取文字、图片、语音、视频等内容中的商品信息并做精准推荐并无难度,可能泄露个人信息的“重灾区”主要集中在应用软件、输入法、公开WiFi、运营商等方面。对于电商广告投放平台来说,接入第三方数据库的行为非常普遍,基本可以算是标配。 1 无处不在的精准推荐 近日,用户A称,8月14日下午,有朋友在微信群里询问“赤峰有没有蕉下伞专柜”,他回答称“不太清楚”。第二天上午,他就收到当当网的短信,推送了“蕉下小黑伞清仓99元”的购买链接。 用户A表示,除了在微信群里和朋友互动之外,他没有在任何地方搜索、浏览过雨伞。“收到当当短信的那一刻,直觉告诉我,我被监控了。” 事后,当当网客服表示,当当网每期的推送是根据平台的促销活动随机发出,用户在当当网的搜索和购买记录平台能获取,但在其它平台上的痕迹并不能获取到。微信团队尚未对此事给出回复。 用户A的案例只是“被监控”的一种情况,用户B则是相册信息被读取。她穿了一套新衣服,拍了一些照片存在手机相册里,随后她打开淘宝首页,推荐的商品均是该款衣服或类似款式。 汪雨表示,她有一次在跟淘宝客服进行售后交涉时候需要上传照片,所以打开了淘宝访问相册的权限,“我授权该权限是出于购物沟通服务的目的,并没有同意平台用我的相册内容推送广告,更不知道他会不会转给第三方或用作其它用途。” 类似的情况时有发生,有用户称刚发布一条表示希望阿迪达斯可以把某款鞋的设计师请回来,并配了相关图片,随后就收到了该鞋的推荐广告。 以上种种案例表明,除了我们日常搜索、浏览、购物之外,相册照片、微信私人聊天记录和群聊记录、物理对话、在社交平台上发布的文字和图片等,都有可能被广告盯上,在互联网包围下的我们仿佛变成了“透明人”。 2 应用软件、输入法、Wi-Fi 成信息泄露重灾区 电商平台是怎么获取用户数据的? 对于相册照片被读取的情况,淘宝或者这些大平台的隐私协议都写了会获取用户的内容,这中间是有灰色地带的,有可能一些软件借着正当业务需求获得了用户权限之后,再用于广告投放或其它用途,但它用隐私条款巧妙回避了法律责任,打了擦边球。一些软件获取了用户的相册、话筒权限后,识别照片、语音是一件很容易的事。 一些第三方输入法,包括为了提升用户体验开启的云词库等都可能是信息泄露的方式,输入法在免费给用户使用的同时也可能在做一些盈利的事。 公开WiFi也是泄露个人信息的一大风险,很多时候免费连WiFi或者手机开启了WiFi模式,就会自动去检索附近的热点,一旦碰上,WiFi都可以收取一定程度的数据。 此外,安卓手机上的很多应用可以开启特殊权限,是隐私泄露的重灾区。它们可能通过一些应用获取到信息,包括聊天消息上传到云端,AI快速精准识别关键字,推送信息给广告商。 3 网络行为怎么变成广告推荐? 拿输入法来说,输入法的数据类似于一个第三方DMP(数据管理平台),拿到这个数据后,还要进行一次数据库匹配,例如和一部手机的Device ID匹配,如果发现用户之前输入过某品牌的雨伞,就可以推送这款产品给用户。而电商广告投放平台接入第三方DMP的行为非常普遍,基本可以算是标配。 百度、阿里、腾讯等都会将平台上的用户行为打标签做成数据库对第三方开放,付费后就能共享信息。 “我们的广告投放主要通过大数据分析,比如通过阿里数据库得到一部分喜欢网购的人的标签,包括他们的购物习惯、消费行为、地域,比如一个用户常浏览汽车、珠宝等品类,消费客单价较高,可能会被定位成高端消费群体,我们找到这些人平时刷什么样的平台,针对性地去投放广告。” 具体的投放方式可以基于地理位置,或是依据数据定向投放。 另一种方式是可以通过一些数据知道这些人的设备里下载了哪些APP,如果很大比例的人下载了某一款,就可以去该平台投放广告。举例来说,假如某平台有5000万注册用户,经检测发现这些人中有大部分下载了知乎、豆瓣,第三方广告商就可以去这两个平台上投放广告,再结合用户在百度的浏览记录、淘宝的购买记录等,就会有一个更加精准的用户画像。 腾讯广告投放包括微信和广点通(覆盖微信之外的QQ、腾讯新闻、QQ浏览器、天天快报等腾讯旗下产品),微信上目前的广告是公众号内文和末尾广告、朋友圈广告、小程序贴片广告,公众号是随机投放,按点击付费一次0.5元起。 “腾讯大数据会分析用户的行为,比如用户经常会浏览、搜索、点击、购买支付的是哪些内容,腾讯旗下各产品之间的信息是互通的,所有发生的用户行为就会形成一个用户画像,做成一个标签,比如最终得出某用户的健身情况、学历教育、对美妆护肤的喜好等。” 朋友圈广告投放是先根据品牌需求定向筛选用户,再按广告曝光次数收费,北京和上海这样的核心城市是0.1元/次曝光,广州、深圳、苏州、杭州是重点城市,0.06元/次曝光,下一级普通城市0.03元/次曝光。 也就是说,如果刷朋友圈的时候看到了某品牌的广告,不管有没有点击,都算一次广告曝光,北京的用户刷到一个广告就为微信贡献了1毛钱广告费。 “各种拉票软件、会议软件、文献提供者、新闻阅读软件等的第一步都是要得到用户的各种数据获取的授权才能运用,这些APP从诞生之日起都有着强制性、偷窥目的的恶意,不少隐蔽性非常强,或者存在强制性获取个人信息的情况。”

2024/3/4
articleCard.readMore

Pyenv工具

Python 版本管理工具的主要作用是帮助开发者在同一台机器上管理多个 Python 版本和环境。这对于开发和部署不同项目非常有用,因为不同项目可能依赖不同的 Python 版本或者不同的包版本。具体来说,Python 版本管理工具应有以下功能: (1)避免依赖冲突,不同的项目可能依赖不同版本的库,使用版本管理工具可以创建独立的虚拟环境,避免依赖冲突。 (2)简化开发流程,开发者可以轻松地在不同的 Python 版本之间切换,而不需要重新安装或配置 Python。 (3)便于部署,减少冲突。在开发环境中使用与生产环境相同的 Python 版本和依赖,可以减少部署时出现的问题。 (4)共享环境配置,提高开发环境一致性。可以将环境配置文件(如 requirements.txt 或 pyproject.toml)共享给团队成员,确保大家使用相同的开发环境。 一、工具选择 常见的管理工具有 Pyenv 和 Conda。Pyenv 是当前最流行的 Python 版本管理工具,支持多种 Python 版本,如 CPython、Anaconda、PyPy 等,功能全面且简单易用。Conda 最初由 Anaconda, Inc. 开发,主要用于 Python 和 R 编程语言的软件包(含 Python)及环境管理,特别适合跨平台、多语言项目,Python 版本管理只是其一小部分功能,若仅用于管理 Python 版本,Conda 有些大材小用,且系统较复杂、学习成本略高。相比之下,Pyenv 是常规项目 Python 版本管理的最优选择。 以下详细介绍 Pyenv 的使用方法。 二、Pyenv 安装 建议: 先卸载系统内置的 Python,否则可能导致 pyenv 设置不生效。 1. Windows pyenv 本身是为 Unix 系统设计的。你可以使用 pyenv-win 这个项目,它是 pyenv 的 Windows 版本。 你需要在 PowerShell 中执行以下命令安装 pyenv-win: 1 Invoke-WebRequest -UseBasicParsing -Uri "https://raw.githubusercontent.com/pyenv-win/pyenv-win/master/pyenv-win/install-pyenv-win.ps1" -OutFile "./install-pyenv-win.ps1"; &"./install-pyenv-win.ps1" 重新打开 PowerShell,运行 pyenv –version 检查安装是否成功。 2. Linux 你可以使用以下命令来安装 pyenv: 1 curl https://pyenv.run | bash 之后再将 pyenv 配置到环境变量中并使之生效,执行如下命令: 1 2 3 4 echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc echo 'command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc echo 'eval "$(pyenv init -)"' >> ~/.bashrc source ~/.bashrc 上述配置仅能使 pyenv 在 bash 环境生效,更多 shell 环境配置请参考:Set up your shell environment for Pyenv。配置的本质在于将$PYENV_ROOT 下的 shims 和 bin 目录配置到 PATH 变量中,且 shims 需配置在前。配置后的 PATH 如下: 1 # echo $PATH /root/.pyenv/shims:/root/.pyenv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin 三、Pyenv 基本用法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 ## 查看帮助文档 pyenv ## 查看某个命令帮助文档 pyenv install --help ## 查看版本 pyenv version ## 检查 Python 是否正常运行 python -c "import sys; print(sys.executable)" ## 查看已安装的 Python 版本 pyenv versions ## 查看当前使用的 Python 版本 pyenv version ## 查看所有可用的 Python 版 pyenv install --list ## 安装指定版本 pyenv install 3.9.1 ## 验证 python --version ## 卸载指定版本 pyenv uninstall 3.9.1 ## 全局指定 Python 版本(影响所有项目) pyenv global 3.9.1 ## 局部指定 Python 版本(仅影响当前项目目录),指定后在当前项目目录内创建 .python-version 文件,保存版本信息## 优先级高于 global pyenv local 3.9.1 ## 会话级指定 Python 版本(影响所有项目) pyenv shell 3.9.1 ## 查看 python 的安装目录 pyenv which python ## 重新生成 pyenv 的 shims 目录中的可执行文件 pyenv rehash Python 安装常见问题,可参考:Python common build problems 四、Pyenv 核心原理 -Shims pyenv 通过 Shims 实现了对不同 Python 版本的透明管理和切换。 1. 工作原理 上述环境配置中,在 PATH 环境变量最前面插入一个 shims 目录,$(pyenv root)/shims:$(pyenv root)/bin:/usr/local/bin:/usr/bin:/bin。通过一个称为 rehashing 的过程,pyenv 在该目录中维护垫片,以匹配每个已安装的 Python 版本中的每个 Python 命令,如: python、pip 等。 Shims 是轻量级可执行文件,它只是将你的命令传递给 pyenv。因此,在安装了 pyenv 的情况下,当你运行 pip 时,你的操作系统将执行以下操作: (1)搜索 PATH 环境变量,寻找 pip 可执行文件 (2)在 $(pyenv root)/shims 中找到 pip (3)运行名为 pip 的 shim,它将命令传递给 pyenv 2. 作用 (1)通过使用 Shims,pyenv 可以实现对不同项目使用不同 Python 版本的灵活管理,而不需要手动修改环境变量或路径。 (2)你可以方便地在全局、目录级别甚至是 shell 会话级别设置或切换 Python 版本,极大地方便了开发和测试工作。 3. 示例 (1)假设你在项目 A 中使用 Python 3.8,而在项目 B 中使用 Python 3.9。通过 pyenv 和 Shims,你可以在项目目录中分别设置 Python 版本: 1 2 3 4 # 在项目 A 目录中 pyenv local 3.8.10 # 在项目 B 目录中 pyenv local 3.9.5 (2)当你在项目 A 目录中运行 python 命令时,Shims 会确保调用的是 Python 3.8.10,而在项目 B 目录中则会调用 Python 3.9.5。 通过这种方式,Shims 实现了对不同 Python 版本的透明管理和切换。 五、Pyenv 初始化操作源码解读 1. pyenv init - 用于初始化 pyenv,使其在当前 shell 会话中工作。运行后,执行如下命令(相关说明附在注释中): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 # 1.PATH 变量处理 ## 该脚本将当前的 PATH 变量拆分为一个数组 paths,并赋予 ## 通过遍历 paths 数组,检查每个路径是否为 '/root/.pyenv/shims',如果是,则将其移除 PATH="$(bash --norc -ec 'IFS=:; paths=($PATH); for i in ${!paths[@]}; do if [[ ${paths[i]} == "''/root/.pyenv/shims''" ]]; then unset '\''paths[i]'\''; fi; done; echo "${paths[*]}"')" # # 2. 更新 PATH 变量 ## 将 '/root/.pyenv/shims' 添加到 PATH 变量的最前面 export PATH="/root/.pyenv/shims:${PATH}" ## 设置 PYENV_SHELL 环境变量为 bash,sh 环境下,输出的是 shell export PYENV_SHELL=bash ## sh 环境下,无该行代码,bash 环境下执行改行的作用是:source 命令加载 pyenv 的自动补全脚本 source '/root/.pyenv/libexec/../completions/pyenv.bash' ## 通过 command 命令执行 pyenv rehash(主要作用是重新生成 pyenv 的 shims 目录中的可执行文件),并将错误输出重定向到 /dev/null command pyenv rehash 2>/dev/null # 3. 定义一个 pyenv 函数,该函数根据不同的子命令执行不同的操作 ## 如果子命令是 activate、deactivate、rehash 或 shell,则通过 eval 执行 pyenv "sh-$command" ## 对于其他子命令,直接调用 command pyenv "$command" "$@" pyenv() { local command command="${1:-}" if [ "$#" -gt 0 ]; then shift fi case "$command" in activate|deactivate|rehash|shell) eval "$(pyenv "sh-$command" "$@")" ;; *) command pyenv "$command" "$@" ;; esac } 2. pyenv init –path 用于设置 PYENV_ROOT 环境变量,使得 pyenv 可以找到安装的 Python 版本。pyenv init - 包含 pyenv init --path 操作。 sh 或 bash 环境运行后,执行如下命令(相关说明附在注释中): 1 2 3 4 5 6 7 8 9 10 11 12 ## 该脚本将当前的 PATH 变量拆分为一个数组 paths,并赋予 ## 通过遍历 paths 数组,检查每个路径是否为 '/root/.pyenv/shims',如果是,则将其移除 PATH="$(bash --norc -ec 'IFS=:; paths=($PATH); for i in ${!paths[@]}; do if [[ ${paths[i]} == "''/root/.pyenv/shims''" ]]; then unset '\''paths[i]'\''; fi; done; echo "${paths[*]}"')" ## 将 '/root/.pyenv/shims' 添加到 PATH 变量的最前面 export PATH="/root/.pyenv/shims:${PATH}" ## 通过 command 命令执行 pyenv rehash,并将错误输出重定向到 /dev/null command pyenv rehash 2>/dev/null

2024/1/8
articleCard.readMore

ElasticSearch集群原理

1 集群分布式原理 ES集群可以根据节点数, 动态调整主分片与副本数, 做到整个集群有效均衡负载。 单节点状态下: 两个节点状态下, 副本数为1: 三个节点状态下, 副本数为1: 三个节点状态下, 副本数为2: 2 分片处理机制 设置分片大小的时候, 需预先做好容量规划, 如果节点数过多, 分片数过小, 那么新的节点将无法分片, 不能做到水平扩展, 并且单个分片数据量太大, 导致数据重新分配耗时过大。 假设一个集群中有一个主节点、两个数据节点。orders索引的分片分布情况如下所示: 1 2 3 4 5 6 7 PUT orders { "settings":{ "number_of_shards":2, ## 主分片 2 "number_of_replicas":2 ## 副分片总数 4 } } 整个集群中存在P0和P1两个主分片, P0对应的两个R0副本分片, P1对应的是两个R1副本分片。 3 新建索引处理流程 写入的请求会进入主节点, 如果是NODE2副本接收到写请求, 会将它转发至主节点。 主节点接收到请求后, 根据documentId做取模运算(外部没有传递documentId,则会采用内部自增ID), 如果取模结果为P0,则会将写请求转发至NODE3处理。 NODE3节点写请求处理完成之后, 采用异步方式, 将数据同步至NODE1和NODE2节点。 4 读取索引处理流程 读取的请求进入MASTER节点, 会根据取模结果, 将请求转发至不同的节点。 如果取模结果为R0,内部还会有负载均衡处理机制,如果上一次的读取请求是在NODE1的R0, 那么当前请求会转发至NODE2的R0, 保障每个节点都能够均衡的处理请求数据。 读取的请求如果是直接落至副本节点, 副本节点会做判断, 若有数据则返回,没有的话会转发至其他节点处理。

2023/12/20
articleCard.readMore

ElasticSearch集群节点

主节点(或候选主节点) 主节点负责创建索引、删除索引、分配分片、追踪集群中的节点状态等工作, 主节点负荷相对较轻, 客户端请求可以直接发往任何节点, 由对应节点负责分发和返回处理结果。 一个节点启动之后, 采用 Zen Discovery机制去寻找集群中的其他节点, 并与之建立连接, 集群会从候选主节点中选举出一个主节点, 并且一个集群只能选举一个主节点, 在某些情况下, 由于网络通信丢包等问题, 一个集群可能会出现多个主节点, 称为“脑裂现象”, 脑裂会存在丢失数据的可能, 因为主节点拥有最高权限, 它决定了什么时候可以创建索引, 分片如何移动等, 如果存在多个主节点, 就会产生冲突, 容易产生数据丢失。要尽量避免这个问题, 可以通过 discovery.zen.minimum_master_nodes 来设置最少可工作的候选主节点个数。 建议设置为(候选主节点/2) + 1 比如三个候选主节点,该配置项为 (3/2)+1 ,来保证集群中有半数以上的候选主节点, 没有足够的master候选节点, 就不会进行master节点选举,减少脑裂的可能。 主节点的参数设置: 1 2 node.master = true node.data = false 数据节点 数据节点负责数据的存储和CRUD等具体操作,数据节点对机器配置要求比较高、,首先需要有足够的磁盘空间来存储数据,其次数据操作对系统CPU、Memory和IO的性能消耗都很大。通常随着集群的扩大,需要增加更多的数据节点来提高可用性。 数据节点的参数设置: 1 2 node.master = false node.data = true 客户端节点 客户端节点不做候选主节点, 也不做数据节点的节点,只负责请求的分发、汇总等等,增加客户端节点类型更多是为了负载均衡的处理。 1 2 node.master = false node.data = false 提取节点(预处理节点) 能执行预处理管道,有自己独立的任务要执行, 在索引数据之前可以先对数据做预处理操作, 不负责数据存储也不负责集群相关的事务。 协调节点 协调节点,是一种角色,而不是真实的Elasticsearch的节点,不能通过配置项来指定哪个节点为协调节点。集群中的任何节点,都可以充当协调节点的角色。当一个节点A收到用户的查询请求后,会把查询子句分发到其它的节点,然后合并各个节点返回的查询结果,最后返回一个完整的数据集给用户。在这个过程中,节点A扮演的就是协调节点的角色。 ES的一次请求非常类似于Map-Reduce操作。在ES中对应的也是两个阶段,称之为scatter-gather。客户端发出一个请求到集群的任意一个节点,这个节点就是所谓的协调节点,它会把请求转发给含有相关数据的节点(scatter阶段),这些数据节点会在本地执行请求然后把结果返回给协调节点。协调节点将这些结果汇总(reduce)成一个单一的全局结果集(gather阶段) 。 部落节点 在多个集群之间充当联合客户端, 它是一个特殊的客户端 , 可以连接多个集群,在所有连接的集群上执行搜索和其他操作。 部落节点从所有连接的集群中检索集群状态并将其合并成全局集群状态。 掌握这一信息,就可以对所有集群中的节点执行读写操作,就好像它们是本地的。 请注意,部落节点需要能够连接到每个配置的集群中的每个单个节点。

2023/12/18
articleCard.readMore

Elasticsearch Mapping 参数

本文基于 Elasticsearch 6.6.0 analyzer 指定分词器(分析器更合理),对索引和查询都有效。如下,指定ik分词的配置: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 PUT my_index { "mappings": { "my_type": { "properties": { "content": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_max_word" } } } } } normalizer normalizer用于解析前的标准化配置,比如把所有的字符转化为小写等。例子: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 PUT index { "settings": { "analysis": { "normalizer": { "my_normalizer": { "type": "custom", "char_filter": [], "filter": ["lowercase", "asciifolding"] } } } }, "mappings": { "type": { "properties": { "foo": { "type": "keyword", "normalizer": "my_normalizer" } } } } } PUT index/type/1 { "foo": "BÀR" } PUT index/type/2 { "foo": "bar" } PUT index/type/3 { "foo": "baz" } POST index/_refresh GET index/_search { "query": { "match": { "foo": "BAR" } } } BÀR经过normalizer过滤以后转换为bar,文档1和文档2会被搜索到。 boost boost字段用于设置字段的权重,比如,关键字出现在title字段的权重是出现在content字段中权重的2倍,设置mapping如下,其中content字段的默认权重是1. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 PUT my_index { "mappings": { "my_type": { "properties": { "title": { "type": "text", "boost": 2 }, "content": { "type": "text" } } } } } 同样,在查询时指定权重也是一样的: 1 2 3 4 5 6 7 8 9 10 11 POST _search { "query": { "match" : { "title": { "query": "quick brown fox", "boost": 2 } } } } 推荐在查询时指定boost,第一中在mapping中写死,如果不重新索引文档,权重无法修改,使用查询可以实现同样的效果。 coerce coerce属性用于清除脏数据,coerce的默认值是true。整型数字5有可能会被写成字符串“5”或者浮点数5.0.coerce属性可以用来清除脏数据: 字符串会被强制转换为整数 浮点数被强制转换为整数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 PUT my_index { "mappings": { "my_type": { "properties": { "number_one": { "type": "integer" }, "number_two": { "type": "integer", "coerce": false } } } } } PUT my_index/my_type/1 { "number_one": "10" } PUT my_index/my_type/2 { "number_two": "10" } mapping中指定number_one字段是integer类型,虽然插入的数据类型是String,但依然可以插入成功。number_two字段关闭了coerce,因此插入失败。 copy_to copy_to属性用于配置自定义的_all字段。换言之,就是多个字段可以合并成一个超级字段。比如,first_name和last_name可以合并为full_name字段。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 PUT my_index { "mappings": { "my_type": { "properties": { "first_name": { "type": "text", "copy_to": "full_name" }, "last_name": { "type": "text", "copy_to": "full_name" }, "full_name": { "type": "text" } } } } } PUT my_index/my_type/1 { "first_name": "John", "last_name": "Smith" } GET my_index/_search { "query": { "match": { "full_name": { "query": "John Smith", "operator": "and" } } } } doc_values doc_values是为了加快排序、聚合操作,在建立倒排索引的时候,额外增加一个列式存储映射,是一个空间换时间的做法。默认是开启的,对于确定不需要聚合或者排序的字段可以关闭。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 PUT my_index { "mappings": { "my_type": { "properties": { "status_code": { "type": "keyword" }, "session_id": { "type": "keyword", "doc_values": false } } } } } 注:text类型不支持doc_values。 dynamic dynamic属性用于检测新发现的字段,有三个取值: true:新发现的字段添加到映射中。(默认) flase:新检测的字段被忽略。必须显式添加新字段。 strict:如果检测到新字段,就会引发异常并拒绝文档。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 PUT my_index { "mappings": { "my_type": { "dynamic": false, "properties": { "user": { "properties": { "name": { "type": "text" }, "social_networks": { "dynamic": true, "properties": {} } } } } } } } 注:取值如果为strict (非布尔值)要加引号。 文档中有一个之前没有出现过的字段被添加到ELasticsearch之后,文档的type mapping中会自动添加一个新的字段。这个可以通过dynamic属性去控制,dynamic属性为false会忽略新增的字段、dynamic属性为strict会抛出异常。如果dynamic为true的话,ELasticsearch会自动根据字段的值推测出来类型进而确定mapping: JSON格式的数据自动推测的字段类型 null没有字段被添加 true or falseboolean类型 floating类型数字floating类型 integerlong类型 JSON对象object类型 数组由数组中第一个非空值决定 string有可能是date类型(开启日期检测)、double或long类型、text类型、keyword类型 日期检测默认是检测符合以下日期格式的字符串: 1 [ "strict_date_optional_time","yyyy/MM/dd HH:mm:ss Z||yyyy/MM/dd Z"] 例子: 1 2 3 4 5 6 PUT my_index/my_type/1 { "create_date": "2015/09/02" } GET my_index/_mapping mapping 如下,可以看到create_date为date类型: 1 2 3 4 5 6 7 8 9 10 { "my_index": { "mappings": { "my_type": { "properties": { "create_date": { "type": "date", "format": "yyyy/MM/dd HH:mm:ss||yyyy/MM/dd||epoch_millis" } } } } } } 关闭日期检测: 1 2 3 4 5 6 7 8 9 10 11 12 13 PUT my_index { "mappings": { "my_type": { "date_detection": false } } } PUT my_index/my_type/1 { "create": "2015/09/02" } 再次查看mapping,create字段已不再是date类型: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 GET my_index/_mapping 返回结果: { "my_index": { "mappings": { "my_type": { "date_detection": false, "properties": { "create": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } } } } } } } 自定义日期检测的格式: 1 2 3 4 5 6 7 8 9 10 11 12 13 PUT my_index { "mappings": { "my_type": { "dynamic_date_formats": ["MM/dd/yyyy"] } } } PUT my_index/my_type/1 { "create_date": "09/25/2015" } 开启数字类型自动检测: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 PUT my_index { "mappings": { "my_type": { "numeric_detection": true } } } PUT my_index/my_type/1 { "my_float": "1.0", "my_integer": "1" } enabled ELasticseaech默认会索引所有的字段,enabled设为false的字段,es会跳过字段内容,该字段只能从_source中获取,但是不可搜。而且字段可以是任意类型。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 PUT my_index { "mappings": { "session": { "properties": { "user_id": { "type": "keyword" }, "last_updated": { "type": "date" }, "session_data": { "enabled": false } } } } } PUT my_index/session/session_1 { "user_id": "kimchy", "session_data": { "arbitrary_object": { "some_array": [ "foo", "bar", { "baz": 2 } ] } }, "last_updated": "2015-12-06T18:20:22" } PUT my_index/session/session_2 { "user_id": "jpountz", "session_data": "none", "last_updated": "2015-12-06T18:22:13" } fielddata 搜索要解决的问题是“包含查询关键词的文档有哪些?”,聚合恰恰相反,聚合要解决的问题是“文档包含哪些词项”,大多数字段再索引时生成doc_values,但是text字段不支持doc_values。 取而代之,text字段在查询时会生成一个fielddata的数据结构,fielddata在字段首次被聚合、排序、或者使用脚本的时候生成。ELasticsearch通过读取磁盘上的倒排记录表重新生成文档词项关系,最后在Java堆内存中排序。 text字段的fielddata属性默认是关闭的,开启fielddata非常消耗内存。在你开启text字段以前,想清楚为什么要在text类型的字段上做聚合、排序操作。大多数情况下这么做是没有意义的。 “New York”会被分析成“new”和“york”,在text类型上聚合会分成“new”和“york”2个桶,也许你需要的是一个“New York”。这是可以加一个不分词的keyword字段: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 PUT my_index { "mappings": { "my_type": { "properties": { "my_field": { "type": "text", "fields": { "keyword": { "type": "keyword" } } } } } } } 上面的mapping中实现了通过my_field字段做全文搜索,my_field.keyword做聚合、排序和使用脚本。 format format属性主要用于格式化日期: 1 2 3 4 5 6 7 8 9 10 11 12 13 PUT my_index { "mappings": { "my_type": { "properties": { "date": { "type": "date", "format": "yyyy-MM-dd" } } } } } 更多内置的日期格式:https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-date-format.html ignore_above ignore_above用于指定字段索引和存储的长度最大值,超过最大值的会被忽略: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 PUT my_index { "mappings": { "my_type": { "properties": { "message": { "type": "keyword", "ignore_above": 15 } } } } } PUT my_index/my_type/1 { "message": "Syntax error" } PUT my_index/my_type/2 { "message": "Syntax error with some long stacktrace" } GET my_index/_search { "size": 0, "aggs": { "messages": { "terms": { "field": "message" } } } } mapping中指定了ignore_above字段的最大长度为15,第一个文档的字段长小于15,因此索引成功,第二个超过15,因此不索引,返回结果只有”Syntax error”,结果如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 { "took": 2, "timed_out": false, "_shards": { "total": 5, "successful": 5, "failed": 0 }, "hits": { "total": 2, "max_score": 0, "hits": [] }, "aggregations": { "messages": { "doc_count_error_upper_bound": 0, "sum_other_doc_count": 0, "buckets": [] } } } ignore_malformed ignore_malformed可以忽略不规则数据,对于login字段,有人可能填写的是date类型,也有人填写的是邮件格式。给一个字段索引不合适的数据类型发生异常,导致整个文档索引失败。如果ignore_malformed参数设为true,异常会被忽略,出异常的字段不会被索引,其它字段正常索引。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 PUT my_index { "mappings": { "my_type": { "properties": { "number_one": { "type": "integer", "ignore_malformed": true }, "number_two": { "type": "integer" } } } } } PUT my_index/my_type/1 { "text": "Some text value", "number_one": "foo" } PUT my_index/my_type/2 { "text": "Some text value", "number_two": "foo" } 上面的例子中number_one接受integer类型,ignore_malformed属性设为true,因此文档一种number_one字段虽然是字符串但依然能写入成功;number_two接受integer类型,默认ignore_malformed属性为false,因此写入失败。 include_in_all include_in_all属性用于指定字段是否包含在_all字段里面,默认开启,除索引时index属性为no。 例子如下,title和content字段包含在_all字段里,date不包含。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 PUT my_index { "mappings": { "my_type": { "properties": { "title": { "type": "text" }, "content": { "type": "text" }, "date": { "type": "date", "include_in_all": false } } } } } include_in_all也可用于字段级别,如下my_type下的所有字段都排除在_all字段之外,author.first_name 和author.last_name 包含在in _all中: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 PUT my_index { "mappings": { "my_type": { "include_in_all": false, "properties": { "title": { "type": "text" }, "author": { "include_in_all": true, "properties": { "first_name": { "type": "text" }, "last_name": { "type": "text" } } }, "editor": { "properties": { "first_name": { "type": "text" }, "last_name": { "type": "text", "include_in_all": true } } } } } } } index index属性指定字段是否索引,不索引也就不可搜索,取值可以为true或者false。 index_options index_options控制索引时存储哪些信息到倒排索引中,接受以下配置: 参数作用 docs只存储文档编号 freqs存储文档编号和词项频率 positions文档编号、词项频率、词项的位置被存储,偏移位置可用于临近搜索和短语查询 offsets文档编号、词项频率、词项的位置、词项开始和结束的字符位置都被存储,offsets设为true会使用Postings highlighter fields fields可以让同一文本有多种不同的索引方式,比如一个String类型的字段,可以使用text类型做全文检索,使用keyword类型做聚合和排序。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 PUT my_index { "mappings": { "my_type": { "properties": { "city": { "type": "text", "fields": { "raw": { "type": "keyword" } } } } } } } PUT my_index/my_type/1 { "city": "New York" } PUT my_index/my_type/2 { "city": "York" } GET my_index/_search { "query": { "match": { "city": "york" } }, "sort": { "city.raw": "asc" }, "aggs": { "Cities": { "terms": { "field": "city.raw" } } } } norms norms参数用于标准化文档,以便查询时计算文档的相关性。norms虽然对评分有用,但是会消耗较多的磁盘空间,如果不需要对某个字段进行评分,最好不要开启norms。 null_value 值为null的字段不索引也不可以搜索,null_value参数可以让值为null的字段显式的可索引、可搜索。例子: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 PUT my_index { "mappings": { "my_type": { "properties": { "status_code": { "type": "keyword", "null_value": "NULL" } } } } } PUT my_index/my_type/1 { "status_code": null } PUT my_index/my_type/2 { "status_code": [] } GET my_index/_search { "query": { "term": { "status_code": "NULL" } } } 文档1可以被搜索到,因为status_code的值为null,文档2不可以被搜索到,因为status_code为空数组,但不是null。 position_increment_gap 为了支持近似或者短语查询,text字段被解析的时候会考虑此项的位置信息。举例,一个字段的值为数组类型: 1 "names": [ "John Abraham", "Lincoln Smith"] 为了区别第一个字段和第二个字段,Abraham和Lincoln在索引中有一个间距,默认是100。例子如下,这是查询”Abraham Lincoln”是查不到的: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 PUT my_index/groups/1 { "names": [ "John Abraham", "Lincoln Smith"] } GET my_index/groups/_search { "query": { "match_phrase": { "names": { "query": "Abraham Lincoln" } } } } 指定间距大于100可以查询到: 1 2 3 4 5 6 7 8 9 10 11 GET my_index/groups/_search { "query": { "match_phrase": { "names": { "query": "Abraham Lincoln", "slop": 101 } } } } 在mapping中通过position_increment_gap参数指定间距: 1 2 3 4 5 6 7 8 9 10 11 12 13 PUT my_index { "mappings": { "groups": { "properties": { "names": { "type": "text", "position_increment_gap": 0 } } } } } properties 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 PUT my_index { "mappings": { "my_type": { "properties": { "manager": { "properties": { "age": { "type": "integer" }, "name": { "type": "text" } } }, "employees": { "type": "nested", "properties": { "age": { "type": "integer" }, "name": { "type": "text" } } } } } } } 对应的文档结构: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 PUT my_index/my_type/1 { "region": "US", "manager": { "name": "Alice White", "age": 30 }, "employees": [ { "name": "John Smith", "age": 34 }, { "name": "Peter Brown", "age": 26 } ] } 可以对manager.name、manager.age做搜索、聚合等操作。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 GET my_index/_search { "query": { "match": { "manager.name": "Alice White" } }, "aggs": { "Employees": { "nested": { "path": "employees" }, "aggs": { "Employee Ages": { "histogram": { "field": "employees.age", "interval": 5 } } } } } } search_analyzer 大多数情况下索引和搜索的时候应该指定相同的分析器,确保query解析以后和索引中的词项一致。但是有时候也需要指定不同的分析器,例如使用edge_ngram过滤器实现自动补全。 默认情况下查询会使用analyzer属性指定的分析器,但也可以被search_analyzer覆盖。例子: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 PUT my_index { "settings": { "analysis": { "filter": { "autocomplete_filter": { "type": "edge_ngram", "min_gram": 1, "max_gram": 20 } }, "analyzer": { "autocomplete": { "type": "custom", "tokenizer": "standard", "filter": [ "lowercase", "autocomplete_filter" ] } } } }, "mappings": { "my_type": { "properties": { "text": { "type": "text", "analyzer": "autocomplete", "search_analyzer": "standard" } } } } } PUT my_index/my_type/1 { "text": "Quick Brown Fox" } GET my_index/_search { "query": { "match": { "text": { "query": "Quick Br", "operator": "and" } } } } similarity BM25 :ES和Lucene默认的评分模型 classic :TF/IDF评分 boolean:布尔模型评分 例子 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 PUT my_index { "mappings": { "my_type": { "properties": { "default_field": { "type": "text" }, "classic_field": { "type": "text", "similarity": "classic" }, "boolean_sim_field": { "type": "text", "similarity": "boolean" } } } } } default_field自动使用BM25评分模型,classic_field使用TF/IDF经典评分模型,boolean_sim_field使用布尔评分模型。 store 默认情况下,自动是被索引的也可以搜索,但是不存储,这也没关系,因为_source字段里面保存了一份原始文档。在某些情况下,store参数有意义,比如一个文档里面有title、date和超大的content字段,如果只想获取title和date,可以这样: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 PUT my_index { "mappings": { "my_type": { "properties": { "title": { "type": "text", "store": true }, "date": { "type": "date", "store": true }, "content": { "type": "text" } } } } } PUT my_index/my_type/1 { "title": "Some short title", "date": "2015-01-01", "content": "A very long content field..." } GET my_index/_search { "stored_fields": [ "title", "date" ] } 查询结果: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 { "took": 1, "timed_out": false, "_shards": { "total": 5, "successful": 5, "failed": 0 }, "hits": { "total": 1, "max_score": 1, "hits": [ { "_index": "my_index", "_type": "my_type", "_id": "1", "_score": 1, "fields": { "date": [ "2015-01-01T00:00:00.000Z" ], "title": [ "Some short title" ] } } ] } } Stored fields返回的总是数组,如果想返回原始字段,还是要从_source中取。 term_vector 词向量包含了文本被解析以后的以下信息: 词项集合 词项位置 词项的起始字符映射到原始文档中的位置。 term_vector参数有以下取值: 参数取值含义 no默认值,不存储词向量 yes只存储词项集合 with_positions存储词项和词项位置 with_offsets词项和字符偏移位置 with_positions_offsets存储词项、词项位置、字符偏移位置 例子: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 PUT my_index { "mappings": { "my_type": { "properties": { "text": { "type": "text", "term_vector": "with_positions_offsets" } } } } } PUT my_index/my_type/1 { "text": "Quick brown fox" } GET my_index/_search { "query": { "match": { "text": "brown fox" } }, "highlight": { "fields": { "text": {} } } } 动态Mapping _default_ 在mapping中使用default字段,那么其它字段会自动继承default中的设置。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 PUT my_index { "mappings": { "_default_": { "_all": { "enabled": false } }, "user": {}, "blogpost": { "_all": { "enabled": true } } } } 上面的 mapping 中,_default_ 中关闭了 _all 字段,user会继承 _default_ 中的配置,因此 user 中的 _all 字段也是关闭的,blogpost 中开启 _all,覆盖了 _default 的默认配置。 当default被更新以后,只会对后面新加的文档产生作用。 dynamic_templates 动态模板可以根据字段名称设置mapping,如下对于string类型的字段,设置mapping为: 1 "mapping": { "type": "long"} 但是匹配字段名称为long_格式的,不匹配_text格式的: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 PUT my_index { "mappings": { "my_type": { "dynamic_templates": [ { "longs_as_strings": { "match_mapping_type": "string", "match": "long_*", "unmatch": "*_text", "mapping": { "type": "long" } } } ] } } } PUT my_index/my_type/1 { "long_num": "5", "long_text": "foo" }12345678910111213141516171819202122232425 写入文档以后,long_num字段为long类型,long_text 仍为string类型。

2023/11/20
articleCard.readMore

Elasticsearch元数据

Meta-Fields(元数据) 本文基于 Elasticsearch 6.6.0 _all _all字段是把其它字段拼接在一起的超级字段,所有的字段用空格分开,_all字段会被解析和索引,但是不存储。当你只想返回包含某个关键字的文档但是不明确地搜某个字段的时候就需要使用_all字段。 例子: 1 2 3 4 5 6 PUT my_index/blog/1 { "title": "Master Java", "content": "learn java", "author": "Tom" } _all字段包含:[ “Master”, “Java”, “learn”, “Tom” ] 搜索: 1 2 3 4 5 6 7 8 GET my_index/_search { "query": { "match": { "_all": "Java" } } } 返回结果: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 { "took": 1, "timed_out": false, "_shards": { "total": 5, "successful": 5, "failed": 0 }, "hits": { "total": 1, "max_score": 0.39063013, "hits": [ { "_index": "my_index", "_type": "blog", "_id": "1", "_score": 0.39063013, "_source": { "title": "Master Java", "content": "learn java", "author": "Tom" } } ] } } 使用copy_to自定义_all字段: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 PUT myindex { "mappings": { "mytype": { "properties": { "title": { "type": "text", "copy_to": "full_content" }, "content": { "type": "text", "copy_to": "full_content" }, "full_content": { "type": "text" } } } } } PUT myindex/mytype/1 { "title": "Master Java", "content": "learn Java" } GET myindex/_search { "query": { "match": { "full_content": "java" } } } _field_names _field_names字段用来存储文档中的所有非空字段的名字,这个字段常用于exists查询。例子如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 PUT my_index/my_type/1 { "title": "This is a document" } PUT my_index/my_type/2?refresh=true { "title": "This is another document", "body": "This document has a body" } GET my_index/_search { "query": { "terms": { "_field_names": [ "body" ] } } } 结果会返回第二条文档,因为第一条文档没有title字段。 同样,可以使用exists查询: 1 2 3 4 5 6 GET my_index/_search { "query": { "exists" : { "field" : "body" } } } _id 每条被索引的文档都有一个_type和_id字段,_id可以用于term查询、temrs查询、match查询、query_string查询、simple_query_string查询,但是不能用于聚合、脚本和排序。例子如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 PUT my_index/my_type/1 { "text": "Document with ID 1" } PUT my_index/my_type/2 { "text": "Document with ID 2" } GET my_index/_search { "query": { "terms": { "_id": [ "1", "2" ] } } } _index 多索引查询时,有时候只需要在特地索引名上进行查询,_index字段提供了便利,也就是说可以对索引名进行term查询、terms查询、聚合分析、使用脚本和排序。 _index是一个虚拟字段,不会真的加到索引中,对_index进行term、terms查询(也包括match、query_string、simple_query_string),但是不支持prefix、wildcard、regexp和fuzzy查询。 举例,2个索引2条文档 1 2 3 4 5 6 7 8 9 PUT index_1/my_type/1 { "text": "Document in index 1" } PUT index_2/my_type/2 { "text": "Document in index 2" } 对索引名做查询、聚合、排序并使用脚本新增字段: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 GET index_1,index_2/_search { "query": { "terms": { "_index": ["index_1", "index_2"] } }, "aggs": { "indices": { "terms": { "field": "_index", "size": 10 } } }, "sort": [ { "_index": { "order": "asc" } } ], "script_fields": { "index_name": { "script": { "lang": "painless", "inline": "doc['_index']" } } } } _meta _parent _parent用于指定同一索引中文档的父子关系。下面例子中现在mapping中指定文档的父子关系,然后索引父文档,索引子文档时指定父id,最后根据子文档查询父文档。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 PUT my_index { "mappings": { "my_parent": {}, "my_child": { "_parent": { "type": "my_parent" } } } } PUT my_index/my_parent/1 { "text": "This is a parent document" } PUT my_index/my_child/2?parent=1 { "text": "This is a child document" } PUT my_index/my_child/3?parent=1&refresh=true { "text": "This is another child document" } GET my_index/my_parent/_search { "query": { "has_child": { "type": "my_child", "query": { "match": { "text": "child document" } } } } } _routing 路由参数,ELasticsearch通过以下公式计算文档应该分到哪个分片上: 1 shard_num = hash(_routing) % num_primary_shards 默认的_routing值是文档的_id或者_parent,通过_routing参数可以设置自定义路由。例如,想把user1发布的博客存储到同一个分片上,索引时指定routing参数,查询时在指定路由上查询: 1 2 3 4 5 6 PUT my_index/my_type/1?routing=user1&refresh=true { "title": "This is a document" } GET my_index/my_type/1?routing=user1 在查询的时候通过routing参数查询: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 GET my_index/_search { "query": { "terms": { "_routing": [ "user1" ] } } } GET my_index/_search?routing=user1,user2 { "query": { "match": { "title": "document" } } } 在Mapping中指定routing为必须的: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 PUT my_index2 { "mappings": { "my_type": { "_routing": { "required": true } } } } PUT my_index2/my_type/1 { "text": "No routing value provided" } _source 存储的文档的原始值。默认_source字段是开启的,也可以关闭: 1 2 3 4 5 6 7 8 9 10 PUT tweets { "mappings": { "tweet": { "_source": { "enabled": false } } } } 但是一般情况下不要关闭,除非你不想做以下操作: 使用update、update_by_query、reindex 使用高亮 数据备份、改变mapping、升级索引 通过原始字段debug查询或者聚合 _type 每条被索引的文档都有一个_type和_id字段,可以根据_type进行查询、聚合、脚本和排序。例子如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 PUT my_index/type_1/1 { "text": "Document with type 1" } PUT my_index/type_2/2?refresh=true { "text": "Document with type 2" } GET my_index/_search { "query": { "terms": { "_type": [ "type_1", "type_2" ] } }, "aggs": { "types": { "terms": { "field": "_type", "size": 10 } } }, "sort": [ { "_type": { "order": "desc" } } ], "script_fields": { "type": { "script": { "lang": "painless", "inline": "doc['_type']" } } } } _uid _uid 即 _type + _id 的组合,可用于查询、聚合、脚本和排序。例子如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 PUT my_index/my_type/1 { "text": "Document with ID 1" } PUT my_index/my_type/2?refresh=true { "text": "Document with ID 2" } GET my_index/_search { "query": { "terms": { "_uid": [ "my_type#1", "my_type#2" ] } }, "aggs": { "UIDs": { "terms": { "field": "_uid", "size": 10 } } }, "sort": [ { "_uid": { "order": "desc" } } ], "script_fields": { "UID": { "script": { "lang": "painless", "inline": "doc['_uid']" } } } }

2023/11/16
articleCard.readMore

Elasticsearch的数据类型

本文基于 Elasticsearch 6.6.0 1 核心数据类型 1.1 字符串类型 - string(不再支持) (1) 使用示例: 1 2 3 4 5 6 7 8 9 10 11 PUT website { "mappings": { "blog": { "properties": { "title": {"type": "string"}, // 全文本 "tags": {"type": "string", "index": "not_analyzed"}// 关键字, 不分词 } } } } (2) ES 5.6.10中的响应信息: 1 2 3 4 5 6 7 #! Deprecation: The [string] field is deprecated, please use [text] or [keyword] instead on [tags] #! Deprecation: The [string] field is deprecated, please use [text] or [keyword] instead on [title] { "acknowledged": true, "shards_acknowledged": true, "index": "website" } (3) ES 6.6.0中的响应信息: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 { "error": { "root_cause": [ { "type": "mapper_parsing_exception", "reason": "No handler for type [string] declared on field [title]" } ], "type": "mapper_parsing_exception", "reason": "Failed to parse mapping [blog]: No handler for type [string] declared on field [title]", "caused_by": { "type": "mapper_parsing_exception", "reason": "No handler for type [string] declared on field [title]" } }, "status": 400 } 可知string类型的field已经被移除了, 我们需要用text或keyword类型来代替string. 1.1.1 文本类型 - text 在Elasticsearch 5.4 版本开始, text取代了需要分词的string. —— 当一个字段需要用于全文搜索(会被分词), 比如产品名称、产品描述信息, 就应该使用text类型. text的内容会被分词, 可以设置是否需要存储: "index": "true|false". text类型的字段不能用于排序, 也很少用于聚合. 使用示例: 1 2 3 4 5 6 7 8 9 10 PUT website { "mappings": { "blog": { "properties": { "summary": {"type": "text", "index": "true"} } } } } 1.1.2 关键字类型 - keyword 在Elasticsearch 5.4 版本开始, keyword取代了不需要分词的string. —— 当一个字段需要按照精确值进行过滤、排序、聚合等操作时, 就应该使用keyword类型. keyword的内容不会被分词, 可以设置是否需要存储: "index": "true|false". 使用示例: 1 2 3 4 5 6 7 8 9 10 PUT website { "mappings": { "blog": { "properties": { "tags": {"type": "keyword", "index": "true"} } } } } 1.2 数字类型 - 8种 数字类型有如下分类: 类型说明 byte有符号的8位整数, 范围: [-128 ~ 127] short有符号的16位整数, 范围: [-32768 ~ 32767] integer有符号的32位整数, 范围: [−231 ~ 231-1] long有符号的64位整数, 范围: [−263 ~ 263-1] float32位单精度浮点数 double64位双精度浮点数 half_float16位半精度IEEE 754浮点类型 scaled_float缩放类型的的浮点数, 比如price字段只需精确到分, 57.34缩放因子为100, 存储结果为5734 使用注意事项: 尽可能选择范围小的数据类型, 字段的长度越短, 索引和搜索的效率越高; 优先考虑使用带缩放因子的浮点类型. 使用示例: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 PUT shop { "mappings": { "book": { "properties": { "name": {"type": "text"}, "quantity": {"type": "integer"}, // integer类型 "price": { "type": "scaled_float", // scaled_float类型 "scaling_factor": 100 } } } } } 1.3 日期类型 - date JSON没有日期数据类型, 所以在ES中, 日期可以是: 包含格式化日期的字符串, “2018-10-01”, 或”2018/10/01 12:10:30”. 代表时间毫秒数的长整型数字. 代表时间秒数的整数. 如果时区未指定, 日期将被转换为UTC格式, 但存储的却是长整型的毫秒值. 可以自定义日期格式, 若未指定, 则使用默认格式: strict_date_optional_time||epoch_millis (1) 使用日期格式示例: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // 添加映射 PUT website { "mappings": { "blog": { "properties": { "pub_date": {"type": "date"} // 日期类型 } } } } // 添加数据 PUT website/blog/11 { "pub_date": "2018-10-10" } PUT website/blog/12 { "pub_date": "2018-10-10T12:00:00Z" }// Solr中默认使用的日期格式 PUT website/blog/13 { "pub_date": "1589584930103" }// 时间的毫秒值 (2) 多种日期格式: 多个格式使用双竖线||分隔, 每个格式都会被依次尝试, 直到找到匹配的. 第一个格式用于将时间毫秒值转换为对应格式的字符串. 使用示例: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // 添加映射 PUT website { "mappings": { "blog": { "properties": { "date": { "type": "date", // 可以接受如下类型的格式 "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis" } } } } } 1.4 布尔类型 - boolean 可以接受表示真、假的字符串或数字: 真值: true, “true”, “on”, “yes”, “1”… 假值: false, “false”, “off”, “no”, “0”, “”(空字符串), 0.0, 0 1.5 二进制型 - binary 二进制类型是Base64编码字符串的二进制值, 不以默认的方式存储, 且不能被搜索. 有2个设置项: (1) doc_values: 该字段是否需要存储到磁盘上, 方便以后用来排序、聚合或脚本查询. 接受true和false(默认); (2) store: 该字段的值是否要和_source分开存储、检索, 意思是除了_source中, 是否要单独再存储一份. 接受true或false(默认). 使用示例: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // 添加映射 PUT website { "mappings": { "blog": { "properties": { "blob": {"type": "binary"} // 二进制 } } } } // 添加数据 PUT website/blog/1 { "title": "Some binary blog", "blob": "hED903KSrA084fRiD5JLgY==" } 注意: Base64编码的二进制值不能嵌入换行符\n, 逗号(0x2c)等符号. 1.6 范围类型 - range range类型支持以下几种: 类型范围 integer_range−231 ~ 231−1 long_range−263 ~ 263−1 float_range32位单精度浮点型 double_range64位双精度浮点型 date_range64位整数, 毫秒计时 ip_rangeIP值的范围, 支持IPV4和IPV6, 或者这两种同时存在 (1) 添加映射: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 PUT company { "mappings": { "department": { "properties": { "expected_number": { // 预期员工数 "type": "integer_range" }, "time_frame": { // 发展时间线 "type": "date_range", "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis" }, "ip_whitelist": { // ip白名单 "type": "ip_range" } } } } } (2) 添加数据: 1 2 3 4 5 6 7 8 9 10 11 12 13 PUT company/department/1 { "expected_number" : { "gte" : 10, "lte" : 20 }, "time_frame" : { "gte" : "2018-10-01 12:00:00", "lte" : "2018-11-01" }, "ip_whitelist": "192.168.0.0/16" } (3) 查询数据: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 GET company/department/_search { "query": { "term": { "expected_number": { "value": 12 } } } } GET company/department/_search { "query": { "range": { "time_frame": { "gte": "208-08-01", "lte": "2018-12-01", "relation": "within" } } } } 查询结果: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 { "took": 26, "timed_out": false, "_shards": { "total": 5, "successful": 5, "skipped": 0, "failed": 0 }, "hits": { "total": 1, "max_score": 1.0, "hits": [ { "_index": "company", "_type": "department", "_id": "1", "_score": 1.0, "_source": { "expected_number": { "gte": 10, "lte": 20 }, "time_frame": { "gte": "2018-10-01 12:00:00", "lte": "2018-11-01" }, "ip_whitelist" : "192.168.0.0/16" } } ] } } 2 复杂数据类型 2.1 数组类型 - array ES中没有专门的数组类型, 直接使用[]定义即可; 数组中所有的值必须是同一种数据类型, 不支持混合数据类型的数组: ① 字符串数组: [“one”, “two”]; ② 整数数组: [1, 2]; ③ 由数组组成的数组: [1, [2, 3]], 等价于[1, 2, 3]; ④ 对象数组: [{“name”: “Tom”, “age”: 20}, {“name”: “Jerry”, “age”: 18}]. 注意: 动态添加数据时, 数组中第一个值的类型决定整个数组的类型; 不支持混合数组类型, 比如[1, “abc”]; 数组可以包含null值, 空数组[]会被当做missing field —— 没有值的字段. 2.2 对象类型 - object JSON文档是分层的: 文档可以包含内部对象, 内部对象也可以包含内部对象. (1) 添加示例: 1 2 3 4 5 6 7 8 9 PUT employee/developer/1 { "name": "ma_shoufeng", "address": { "region": "China", "location": {"province": "GuangDong", "city": "GuangZhou"} } } (2) 存储方式: 1 2 3 4 5 6 7 { "name": "ma_shoufeng", "address.region": "China", "address.location.province": "GuangDong", "address.location.city": "GuangZhou" } (3) 文档的映射结构类似为: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 PUT employee { "mappings": { "developer": { "properties": { "name": { "type": "text", "index": "true" }, "address": { "properties": { "region": { "type": "keyword", "index": "true" }, "location": { "properties": { "province": { "type": "keyword", "index": "true" }, "city": { "type": "keyword", "index": "true" } } } } } } } } } 2.3 嵌套类型 - nested 嵌套类型是对象数据类型的一个特例, 可以让array类型的对象被独立索引和搜索. 2.3.1 对象数组是如何存储的 ① 添加数据: 1 2 3 4 5 6 7 8 9 PUT game_of_thrones/role/1 { "group": "stark", "performer": [ {"first": "John", "last": "Snow"}, {"first": "Sansa", "last": "Stark"} ] } ② 内部存储结构: 1 2 3 4 5 6 { "group": "stark", "performer.first": [ "john", "sansa" ], "performer.last": [ "snow", "stark" ] } ③ 存储分析: 可以看出, user.first和user.last会被平铺为多值字段, 这样一来, John和Snow之间的关联性就丢失了. 在查询时, 可能出现John Stark的结果. 2.3.2 用nested类型解决object类型的不足 如果需要对以最对象进行索引, 且保留数组中每个对象的独立性, 就应该使用嵌套数据类型. —— 嵌套对象实质是将每个对象分离出来, 作为隐藏文档进行索引. ① 创建映射: 1 2 3 4 5 6 7 8 9 10 11 PUT game_of_thrones { "mappings": { "role": { "properties": { "performer": {"type": "nested" } } } } } ② 添加数据: 1 2 3 4 5 6 7 8 9 PUT game_of_thrones/role/1 { "group" : "stark", "performer" : [ {"first": "John", "last": "Snow"}, {"first": "Sansa", "last": "Stark"} ] } ③ 检索数据: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 GET game_of_thrones/_search { "query": { "nested": { "path": "performer", "query": { "bool": { "must": [ { "match": { "performer.first": "John" }}, { "match": { "performer.last": "Snow" }} ] } }, "inner_hits": { "highlight": { "fields": {"performer.first": {}} } } } } } 3 地理数据类型 3.1 地理点类型 - geo point 地理点类型用于存储地理位置的经纬度对, 可用于: 查找一定范围内的地理点; 通过地理位置或相对某个中心点的距离聚合文档; 将距离整合到文档的相关性评分中; 通过距离对文档进行排序. (1) 添加映射: 1 2 3 4 5 6 7 8 9 10 11 PUT employee { "mappings": { "developer": { "properties": { "location": {"type": "geo_point"} } } } } (2) 存储地理位置: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // 方式一: 纬度 + 经度键值对 PUT employee/developer/1 { "text": "小蛮腰-键值对地理点参数", "location": { "lat": 23.11, "lon": 113.33// 纬度: latitude, 经度: longitude } } // 方式二: "纬度, 经度"的字符串参数 PUT employee/developer/2 { "text": "小蛮腰-字符串地理点参数", "location": "23.11, 113.33" // 纬度, 经度 } // 方式三: ["经度, 纬度"] 数组地理点参数 PUT employee/developer/3 { "text": "小蛮腰-数组参数", "location": [ 113.33, 23.11 ] // 经度, 纬度 } (3) 查询示例: 1 2 3 4 5 6 7 8 9 10 11 12 GET employee/_search { "query": { "geo_bounding_box": { "location": { "top_left": { "lat": 24, "lon": 113 },// 地理盒子模型的上-左边 "bottom_right": { "lat": 22, "lon": 114 }// 地理盒子模型的下-右边 } } } } 3.2 地理形状类型 - geo_shape 是多边形的复杂形状. 使用较少, 这里省略. 4 专门数据类型 4.1 IP类型 IP类型的字段用于存储IPv4或IPv6的地址, 本质上是一个长整型字段. (1) 添加映射: 1 2 3 4 5 6 7 8 9 10 11 PUT employee { "mappings": { "customer": { "properties": { "ip_addr": { "type": "ip" } } } } } (2) 添加数据: 1 2 3 PUT employee/customer/1 { "ip_addr": "192.168.1.1" } (3) 查询数据: 1 2 3 4 5 6 7 GET employee/customer/_search { "query": { "term": { "ip_addr": "192.168.0.0/16" } } } 4.2 计数数据类型 - token_count token_count类型用于统计字符串中的单词数量. 本质上是一个整数型字段, 接受并分析字符串值, 然后索引字符串中单词的个数. (1) 添加映射: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 PUT employee { "mappings": { "customer": { "properties": { "name": { "type": "text", "fields": { "length": { "type": "token_count", "analyzer": "standard" } } } } } } } (2) 添加数据: 1 2 3 4 5 PUT employee/customer/1 { "name": "John Snow" } PUT employee/customer/2 { "name": "Tyrion Lannister" } (3) 查询数据: 1 2 3 4 5 6 7 GET employee/customer/_search { "query": { "term": { "name.length": 2 } } } 参考资料 Elasticsearch 6.6 官方文档 - Field datatypes

2023/11/12
articleCard.readMore

Go语言的向后兼容和toolchain规则

1. Go 1.21版本之前的向前兼容问题 2. Go 1.21版本后的向前兼容策略 3. module依赖的Go toolchain版本的选择过程 4. GOTOOLCHAIN环境变量与toolchain版本选择 5. 小结 6. 参考资料 Go语言在发展演进过程中一直十分注重向后兼容性(backward compatibility),在Go 1.0版本发布之初就发布了Go1兼容性承诺,简单来说就是保证使用新版本Go可以正常编译和运行老版本的Go代码语法编写的go代码),不会出现breaking change(其实也不是绝对的不会出现)。 但是在Go 1.21版本之前,Go语言在向前兼容性方面却存在一定的不确定性问题。Go 1.21版本对此进行了改进,并引入了go toolchain规则。 1. Go 1.21版本之前的向前兼容问题 在Go 1.21版本之前,Go module中的go directive用于声明建议的Go版本,但并不强制实施。例如: 1 2 3 4 // go.mod module demo1 go 1.20 上面go.mod文件中的go directive表示建议使用Go 1.20及以上版本编译本module代码,但并不强制禁止使用低于1.20版本的Go对module进行编译。你也可以使用Go 1.19版本,甚至是Go 1.15版本编译这个module的代码。 但Go官方对于这种使用低版本(L)编译器编译go directive为高版本(H)的Go module的结果没有作出任何承诺和保证,其结果也是不确定的。 如果你在代码中没有引入高版本(>=L+1)go的新语法特性,那么编译是可以通过的。 如果你的代码没有用到任何高版本(>=L+1)的语法行为变更、bug或安全漏洞的代码,那么编译出的可执行程序运行起来也可以是正常的。 否则,可能会编译失败、运行失败, 或者运行时出现breaking change的问题。 自己的代码可以避免这些问题,但如果你的module有外部依赖,就无法避免了。从Go 1.21版本开始,Go团队在向前兼容方面做了改善,以规避编译结果的不确定性。 2. Go 1.21版本后的向前兼容策略 Go从Go 1.11版本引入go module,在go 1.16版本,go module构建模式正式成为默认构建模式,替代了原先的GOPATH构建模式。 注:《Go语言第一课》专栏的第6讲和第7讲对Go module构建模式与6类常规操作做了全面系统的讲解。 通过go module,结合语义导入版本(semantic import versioning)、最小版本选择(Minimal version selection)等机制,go build可以实现精确的依赖管控。 Go 1.21版本后的向前兼容性策略的调整就是参考了go module对依赖的管理方法:即将go版本和go toolchain版本作为一个module的“依赖”来管理。如果你真正理解了这个,那理解后面那些具体的规则就容易多了! 如果Russ Cox当初设计Go module就想到了今天这个思路,估计就会直接使用go.mod文件中的require语法像管理依赖module那样来管理go version和go toolchain了: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // go.mod (假想的) module demo1 require ( go 1.20.5 toolchain go1.21.1 ) require ( github.com/gomodule/redigo v1.8.5 github.com/google/gops v0.3.19 github.com/panjf2000/ants v1.2.1 ) 但时间无法倒流,历史不能重来,Russ Cox现在只能使用go directive和toolchain directive来提供对go版本和go工具链的依赖信息: 1 2 3 4 5 6 // go.mod module demo1 go 1.20.5 toolchain 1.21.1 同时和使用go get可以改变go.mod的require块中的依赖的版本一样,通过go get也可以修改go.mod中go和toolchain指示的版本: 1 2 $go get go@1.21.1 $go get toolchain@go1.22.1 基于上述策略调整,为解决向前兼容不确定性的问题,Go从1.21版本开始,改变了go.mod中go directive的语义:它不再是建议,而是指定了module最小可用的Go版本。 这样在仅使用本地go工具链的情况下,如果Go编译器版本低于go.mod中的go版本,将无法编译代码: 1 2 3 4 5 6 7 8 // go.mod module demo1 go 1.21.1 // 指定最小可用版本为Go 1.21.1 $GOTOOLCHAIN=local go build go: go.mod requires go &gt;= 1.21.1 (running go 1.21.0; GOTOOLCHAIN=local) 这里有一个前提:“在仅使用本地go工具链的情况下(即设置了GOTOOLCHAIN=local)”,在Go 1.21版本之前,我们遇到的都属于这种情况。遇到这种情况后,我们一般的作法是手动下载对应版本的Go工具链(比如这里的go 1.21.1),然后用新版工具链重新编译。 Go团队考虑到手动管理go工具链带来的体验不佳问题,在Go 1.21版本及以后,go还提供了自动Go工具链管理,如果go发现本地工具链版本低于go module要求的最低go版本,那么go会自动下载高版本的go工具链,缓存到go module cache中(不会覆盖本地安装的go工具链),并用新下载的go工具链对module进行编译构建: 1 2 3 4 5 6 7 8 // go.mod module demo1 go 1.21.1 // 指定最小可用版本为Go 1.21.1 $go build go: downloading go1.21.1 (darwin/amd64) 注:从兼容性方面考虑,如果go.mod中没有显式的用go指示go版本,那么默认go版本为1.16。 对应module有依赖的情况,比如下图: 这里要正确编译图中的main module,我们至少需要go 1.21.0版本,这个版本是main所有依赖中version最大的那个。 当然最终选择哪个版本的go工具链对module进行编译,则有一个选择决策的过程。 go module构建模式下,go工具链选择依赖module的版本时有一套机制,比如最小版本选择等,Go 1.21以后,go工具链版本的选择,也有一套类似的逻辑。接下来我们就来简单看一下。 3. module依赖的Go toolchain版本的选择过程 go module中依赖module的版本选择机制:最小版本选择(mvs) 上图来自https://go.dev/ref/mod 以module C的版本选择为例,A依赖C 1.3,B依赖C 1.4,那么满足应用依赖需求的最小版本就是1.4。如果选择1.3,则不满足B对依赖的要求。 对Go toolchain的选择过程也遵循mvs方法,我们把前面的那个例子拿过来: 现在我们帮这个例子选择go toolchain版本。 注:如果go.mod中没有显式用toolchain指示工具链版本,那我们可以认为go.mod中有一个隐含的toolchain指示版本,该版本与go directive指示的版本一致。 上面的例子中对toolchain version的最高要求为module D的go 1.21.0,当startup toolchain(执行的那个go命令的版本)得到这个信息后,就会在当前可用的toolchain版本列表中选出满足go 1.21.0的最小版本的go toolchain,然后会有一个叫Go toolchain switches(Go工具链切换)的过程,切换后,选出的新版go toolchain会继续后面的工作(编译和链接)。例如,如果可用的toolchain版本有如下三个: go 1.22.7 go 1.21.3 go 1.21.5 那么startup toolchain会根据mvs原则选出满足go 1.21.0的最小版本,即go 1.21.3。 这里大家可能会马上问:什么是可用的toolchain版本?别急!接下来我们就来回答这个问题。 4. GOTOOLCHAIN环境变量与toolchain版本选择 是否执行自动工具链下载和缓存、Go toolchain switches(Go工具链切换)以及切换的工具链的版本取决于GOTOOLCHAIN环境变量的设置、go.mod中go和toolchain指示的版本。 当go命令捆绑的工具链与module的go.mod的go或工具链版本一样时或更新时,go命令会使用自己的捆绑工具链。例如,当在main module的go.mod包含有go 1.21.0时,如果go命令绑定的工具链是Go 1.21.3版本,那么将继续使用初始toolchain的版本,即Go 1.21.3。 而如果go.mod中的go版本写着go 1.21.9,那么go命令捆绑的工具链版本1.21.3显然不能满足要求,那此时就要看GOTOOLCHAIN环境变量的配置。 GOTOOLCHAIN的设置以及对结果的影响略复杂,下面是GOTOOLCHAIN的多种设置形式以及对toolchain选择的影响说明(以下示例中本地go命令捆绑的工具链版本为Go 1.21.0): <name> 例如,GOTOOLCHAIN=go1.21.0。go命令将始终运行该特定版本的go工具链。如果本地存在该版本工具链,就使用本地的。如果不存在,会下载、缓存起来并使用。如果go.mod中的工具链版本高于name版本,则停止编译: 1 2 3 4 5 6 7 8 $cat go.mod module demo1 go 1.23.1 toolchain go1.23.1 $GOTOOLCHAIN=go1.21.0 go build go: go.mod requires go &gt;= 1.23.1 (running go 1.21.0; GOTOOLCHAIN=go1.21.0) <name>+auto 当GOTOOLCHAIN设置为<name>+auto时,go命令会根据需要选择并运行较新的Go版本。具体来说,它会查询go.mod文件中的工具链版本和go version。如果go.mod 文件中有toolchain行,且toolchain指示的版本比默认的Go工具链(name)新,那么系统就会调用toolchain指示的工具链版本;反之会使用默认工具链。 当本地不存在决策后的工具链版本时,会自动下载、缓存,并使用该缓存工具链进行后续编译。 1 2 3 4 5 6 7 8 9 10 11 $cat go.mod module demo1 go 1.23.1 toolchain go1.23.1 $GOTOOLCHAIN=go1.24.1+auto go build go: downloading go1.24.1 (darwin/amd64) // 使用name指定工具链,但该工具链本地不存在,于是下载。 $GOTOOLCHAIN=go1.20.1+auto go build go: downloading go1.23.1 (darwin/amd64) // 使用go.mod中的版本的工具链 <name>+path 当GOTOOLCHAIN设置为<name>+path时,go命令会根据需要选择并运行较新的Go版本。具体来说,它会查询go.mod文件中的工具链版本和go version。如果go.mod 文件中有toolchain行,且toolchain指示的版本比默认的Go工具链(name)新,那么系统就会调用toolchain指示的工具链版本;反之会使用默认工具链。当使用<name>+path时,如果决策得到的工具链版本在PATH路径下没有找到,那么go命令执行过程将终止。 1 2 3 4 5 6 7 8 9 10 11 $cat go.mod module demo1 go 1.23.1 toolchain go1.23.1 $GOTOOLCHAIN=go1.24.1+path go build // 使用name指定工具链,但该工具链本地不存在,于是编译停止 go: cannot find "go1.24.1" in PATH $GOTOOLCHAIN=go1.20.1+path go build // 使用go.mod中的版本的工具链,但该工具链本地不存在,于是编译停止 go: cannot find "go1.23.1" in PATH auto (等价于 local+auto,这也是默认值) auto的语义是当go.mod中工具链版本低于go命令捆绑的工具链版本,则使用go命令运行捆绑的工具链;反之,自动下载对应的工具链版本,缓存起来并使用。 1 2 3 4 5 6 7 8 $cat go.mod module demo1 go 1.23.1 toolchain go1.23.1 $GOTOOLCHAIN=auto go build go: downloading go1.23.1 (darwin/amd64) path (等价于 local+path) path的语义是当go.mod中工具链版本低于go命令捆绑的工具链版本,则使用go命令运行捆绑的工具链;反之,在PATH中找到满足go.mod中工具链版本的go版本。如果没找到,则会停止编译过程: 1 2 3 4 5 6 7 8 $cat go.mod module demo1 go 1.23.1 toolchain go1.23.1 $GOTOOLCHAIN=path go build go: cannot find "go1.23.1" in PATH local 当GOTOOLCHAIN设置为local时,go命令总是运行捆绑的 Go 工具链。如果go.mod中工具链版本高于local的版本,则会停止编译过程。 1 2 3 4 5 6 7 8 $cat go.mod module demo1 go 1.23.1 toolchain go1.23.1 $GOTOOLCHAIN=local go build go: go.mod requires go &gt;= 1.23.1 (running go 1.21.0; GOTOOLCHAIN=local) 当Go工具在编译module依赖项时发现当前go toolchain版本无法满足要求时,会进行go toolchain switches(切换),切换的过程就是从可用的go toolchain列表中取出一个最适合的。 那么“可用的go toolchain列表”究竟是如何组成的呢? go命令有三个候选版本(以 Go 1.21.1为例,这些版本也是Go当前承诺提供support的版本): 尚未发布的Go语言版本的最新候选版本(1.22rc1) 最近发布的 Go 语言版本的最新补丁 (1.21.1) 上一个Go语言版本的最新补丁版本(1.20.8)。 当GOTOOLCHAIN设置为带auto形式的值的时候,Go会下载这些版本;当GOTOOLCHAIN设置为代path形式的值的时候,Go会在PATH路径搜索适合的go工具链列表。 接下来,go会用mvs(最小版本选择)来确定究竟使用哪个toolchain版本。Go toolchain reference中就有这样一个例子。 假设example.com/widget@v1.2.3需要Go 1.24rc1或更高版本。go命令会获取可用工具链列表,并发现两个最新Go工具链的最新补丁版本是Go 1.28.3和Go 1.27.9,候选版本Go 1.29rc2也可用。在这种情况下,go 命令会选择Go 1.27.9。 如果 widget 需要 Go 1.28或更高版本,go命令会选择 Go 1.28.3,因为 Go 1.27.9 太旧了。如果widget需要Go 1.29或更高版本,go命令会选择Go 1.29rc2,因为Go 1.27.9和Go 1.28.3都太老了。 5. 小结 Go 1.21通过增强go语句语义和添加工具链管理,大幅改进了Go语言的向前兼容性。开发者可以放心使用新语言特性,无需担心旧版本编译器带来的问题。go命令会自动处理不同module使用不同go版本和不同工具链版本的情况,使用Go语言变得更简单。 6. 参考资料 Forward Compatibility and Toolchain Management in Go 1.21 – https://go.dev/blog/toolchain Go Toolchains reference – https://go.dev/doc/toolchain

2023/10/21
articleCard.readMore

Go 1.21 新增特性

新的内置函数 1.21添加了三个新的内置函数:min、max和clear。 min、max如其字面意思,用了选出一组变量里(数量大于等于1,只有一个变量的时候就返回那个变量的值)最大的或者最小的值。两个函数定义是这样的: 1 2 func min[T cmp.Ordered](x T, y ...T) T func max[T cmp.Ordered](x T, y ...T) T 注意那个类型约束,这是新的标准库里提供的,原型如下: 1 2 3 4 5 6 type Ordered interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string } 也就是说只有基于所有除了map,chan,slice以及复数之外的基本类型的变量才能使用这两个函数。或者换句话说,只有可以使用<、>、<=、>=、==和!=进行比较的类型才可以使用min和max。 有了min和max,可以把许多自己手写的代码替换成新的内置函数,可以少写一些帮助函数。而且使用新的内置函数还有一个好处,在变量个数比较少的时候还有编译器的优化可用,比自己写min函数性能上要稍好一些。 使用上也很简单: 1 2 maxIntValue := max(0, 7, 6, 5, 4, 3, 2, 1) // 7 type int minIntValue := min(8, 7, 6, 5, 4, 3, 2, 1) // 1 type int 目前max和min都不支持slice的解包操作:max(1, numbers...)。 对于clear来说事情比min和max复杂。clear只接受slice和map,如果是对泛型的类型参数使用clear,那么类型参数的type set必须是map或者slice,否则编译报错。 clear的定义如下: 1 func clear[T ~[]Type | ~map[Type]Type1](t T) 对于参数是map的情况,clear会删除所有map里的元素(不过由于golang的map本身的特性,map存储的数据会被正常销毁,但map已经分配的空间不会释放): 1 2 3 4 5 6 func main() { m := map[int]int{1:1, 2:2, 3:3, 4:4, 5:5} fmt.Println(len(m)) // 5 clear(m) fmt.Println(len(m)) // 0 } 然而对于slice,它的行为又不同了:会把slice里所有元素变回零值。看个例子: 1 2 3 4 5 6 7 8 9 func main() { s := make([]int, 0, 100) // 故意给个大的cap便于观察 s = append(s, []int{1, 2, 3, 4, 5}...) fmt.Println(s) // [1 2 3 4 5] fmt.Println(len(s), cap(s)) // len: 5; cap: 100 clear(s) fmt.Println(s) // [0 0 0 0 0] fmt.Println(len(s), cap(s)) // len: 5; cap: 100 } 这个就比较反直觉了,毕竟clear首先想到的应该是把所有元素删除。那它的意义是什么呢?对于map来说意义是很明确的,但对于slice来说就有点绕弯了: slice的真实大小是cap所显示的那个大小,如果只是用s := s[:0]来把所有元素“删除”,那么这些元素实际上还是留在内存里的,直到s本身被gc回收或者往s里添加新元素把之前的对象覆盖掉,否则这些对象是不会被回收掉的,这一方面可以提高内存的利用率,另一方面也会带来泄露的问题(比如存储的是指针类型或者包含指针类型的值的时候,因为指针还存在,所以被指向的对象始终有一个有效的引用导致无法被回收),所以golang选择了折中的办法:把所有已经存在的元素设置成0值 如果想安全的正常删除slice的所有元素,有想复用slice的内存,该怎么办?答案是: 1 2 3 4 5 s := make([]T, 0, 100) // 故意给个大的cap便于观察 s = append(s, []T{*new(T), *new(T)}...) clear(s) s = s[:0] 如果没有内置函数clear,那么我们得自己循环一个个赋值处理。而有clear的好处是,编译器会直接用memset把slice的内存里的数据设置为0,比循环会快很多。有兴趣的可以看看clear在slice上的实现:代码在这 。 类型推导 以前类似这样的代码在某些情况下没法正常进行推导: 1 2 3 4 func F[T ~E[], E any](t T, callable func(E)) func generic[E any](e E) {} F(t, generic) // before go1.21: error; after go1.21: ok 理论上只要能推导出E的类型,那么F和generic的所有类型参数都能推导出来,哪怕generic本身是个泛型函数。以前想正常使用就得这么写:F(t, generic[Type])。 所以与其说是新特性,不如说是对类型推导的bug修复。 针对类型推导还有其他一些修复和报错信息的内容优化,但这些都没上面这个变化大,所以恕不赘述。 panic的行为变化 1.21开始如果goroutine里有panic,那么这个goroutine里的defer里调用的recover必然不会返回nil值。 这导致了一个问题:recover的返回值是传给panic的参数的值,panic(nil)这样的代码怎么办? 先要提醒一下,panic(nil)本身是无意义的,且会导致recover的调用方无法判断究竟发生了什么,所以一直是被各类linter包括go vet命令警告的。然而这么写语法上完全正确,所以只有警告并不能解决问题。 解决办法是,如果现在使用panic(nil)或者panic(值为nil的接口),recover会收到一个新类型的error:*runtime.PanicNilError。 总体上算是解决了问题,然而它把有类型的但值是nil的接口也给转换了,虽然从最佳实践的角度来讲panic一个空值的接口是不应该的,但多少还是会给使用上带来一些麻烦。 所以目前想要禁用这一行为的话,可以设置环境变量:export GODEBUG=panicnil=1。如果go.mod里声明的go版本小于等于1.20,这个环境变量的功能自动启用。 对于modules的变化,会在下一节讲解。 modules的变化 最大的变化是现在mod文件里写的go版本号的意义改变了。 变成了:mod文件里写的go的版本意味着这个mod最低支持的golang版本是多少。 比如: 1 2 3 module github.com/apocelipes/flatmap go 1.21.0 意味着这个modules最低要求go的版本是go1.21.0,而且可以注意到,现在patch版本也算在内里,所以一个声明为go 1.21.1的modules没法被1.21.0版本的go编译。 这么做的好处是能严格控制自己的程序和库可以在哪些版本的golang上运行,且可以推动库版本和golang本身版本的升级。 如果严格按照官方要求使用语义版本来控制自己的modules的话,这个改动没有什么明显的坏处,唯一的缺点是只有1.21及更高版本的go工具链才有这样的功能。 这个变更对go.work文件同样适用。 包初始化顺序的改变 现在按新的顺序来初始化包: 把所有的packages按导入路径进行排序(字符串字典顺序)存进一个列表 按要求和顺序找到列表里第一个符合条件的package,要求是这个package所有的import的包都已经完成初始化 初始化这个找到的包然后把它移出列表,接着重复第二步 列表为空的时候初始化流程结束 这样做的好处是包的初始化顺序终于有明确的标准化的定义了,坏处有两点: 以前的程序如果依赖于特定的初始化顺序,那么在新版本会出问题 现在可以通过修改package的导入路径(主要能改的部分是包的名字)和控制导入的包来做到让A包始终在B包之前初始化,因此B包的初始化流程里可以对A包公开出来的接口或者数据进行修改,这样做耦合性很高也不安全,尤其是B包如果是某个包含恶意代码的包的话。 我们能做的只有遵守最佳实践:不要依赖特定的包直接的初始化顺序;以及在使用某个第三方库前要仔细考量。 编译器和runtime的变化 runtime的变化上,gc一如既往地得到了小幅度优化,现在对于gc压力较大的程序来说gc延迟和内存占用都会有所减少。 cgo也有优化,现在cgo函数调用最大可以比原先快一个数量级。 编译器的变化上比较显著的是这个:PGO已经可以正式投入生产使用。使用教程。 PGO可以带来6%左右的性能提升,1.21凭借PGO和上个版本的优化现在不仅没有了泛型带来的编译速度减低,相比以前还有细微提升。 还有最后一个变化,这个和编译器关系:现在没有被使用的全局的map类型的变量(需要达到一定大小,且初始化的语句中没有任何副作用会产生),现在编译完成的程序里不会在包含这个变量。因为map本身占用内存且初始化需要花费一定时间(map越大花的时间越多)。这个好处是很实在的,既可以减小产生的二进制可执行文件的大小,又可以提升运行性能。但有个缺点,如果有什么程序要依赖编译好的可执行文件内部的某些数据的话,这个变更可能会带来麻烦,普通用户可以忽略这点。 新标准库 这个版本添加了大把的新标准库,一起来看看。 log/slog和testing/slogtest 官方提供的结构化日志库。 可以通过实现slog.Handler来定义自己的日志处理器,可以把日志转换成json等格式。标准库自带了很多预定义的处理器,比如json的: 1 2 3 logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) logger.Info("hello", "count", 3) // {"time":"2023-08-09T15:28:26.000000000+09:00","level":"INFO","msg":"hello","count":3} 简单得说,就是个简化版的zap,如果想使用最基础的结构化日志的功能,又不想引入zap这样的库,那么slog是个很好的选择。 testing/slogtest里有帮助函数用来测试自己实现的日志处理器是否符合标准库的要求。 slices和maps 把golang.org/x/exp/slices和golang.org/x/exp/maps引入了标准库。 slices库提供了排序、二分查找、拼接、增删改查等常用功能,sort这个标准库目前可以停止使用用slices来替代了。 maps提供了常见的对map的增删改查拼接合并等功能。 两个库使用泛型,且针对golang的slice和map进行了细致入微的优化,性能上比自己写的版本有更多优势,比标准库sort更是有数量级的碾压。 这两个库本来1.20就该被接收进标准库了,但因为需要重新设计api和进行优化,所以拖到1.21了。 cmp 这个也是早该进入标准库的,但拖到了现在。随着slices、maps和新内置函数都进入了新版本,这个库想不接收也不行了。 这个库一共有三个东西:Ordered、Less、Compare。 最重要的是Ordered,它是所有可以使用内置运算符进行比较的类型的集合。 Less和Ordered顾名思义用来比大小的,且只能比Ordered类型的大小。 之所以还有单独造出这两个函数,是因为他们对Nan有检查,比如: 1 2 3 4 5 6 // Less reports whether x is less than y. // For floating-point types, a NaN is considered less than any non-NaN, // and -0.0 is not less than (is equal to) 0.0. func Less[T Ordered](x, y T) bool { return (isNaN(x) && !isNaN(y)) || x < y } 所以在泛型函数里不知道要比较的数据的类型是不是有float的时候,用cmp里提供的函数是最安全的。这就是他俩存在的意义。 但如果可以100%确定没有float存在,那么就不应该用Less等,应该直接用运算符去比较,因为大家都看到,Less和直接比较相比效率是较低的。 已有的标准库的变化 因为是速览,所以我只挑重点说。 bytes bytes.Buffer添加了AvailableBuffer和Available两个方法,分别返回目前可用的buf切片和可用的长度。主要可以配合strconv.AppendInt来使用,直接把数据写入buffer对应的内存里,可以提升性能。不要对AvailableBuffer返回的切片扩容,否则必然踩坑。 context 新的context.WithoutCancel会把原来的context.Context复制一份,并去除cancel函数,这意味着原先被复制的上下文取消了这个新的上下文也将继续存在。例子: 1 2 3 4 5 6 7 func main() { ctx, cancel := context.WithCancel(context.Background()) newCtx := context.WithoutCancel(ctx) cancel() <-ctx.Done() // ok, ctx has cancled. <-newCtx.Done() // error: dead lock! } 之所以会死锁,是因为newCtx没有被取消,Done返回的chan会永远阻塞住。而且更根本的,newCtx无法被取消。 新增了context.WithDeadlineCause和context.WithTimeoutCause,可以增加超时上下文被取消时的信息: 1 2 3 4 d := time.Now().Add(shortDuration) ctx, cancel := context.WithDeadline(context.Background(), d, &MyError{"my message"}) cancel() context.Cause(ctx) // --> &MyError{"my message"} 虽然不如context.WithCancelCause灵活,但也很实用。 crypto/sha256 现在在x86_64平台上计算sha256会尽量利用硬件指令(simd和x86_64平台的SHA256ROUND等指令),这带来了3-4倍的性能提升。 net 现在golang在Linux上已经初步支持Multipath TCP。有关Multipath TCP的信息可以在这查阅:https://www.multipath-tcp.org/ reflect ValueOf现在会根据逃逸分析把值分配在栈上,以前都是直接分配到堆上的。对于比较小的类型来说可以获得10%以上的性能提升。利好很多使用反射的ORM框架。 新增了Value.Clear,对应第一节的clear内置函数,如果type不是map或者slice的话这个函数和其他反射的方法一样会panic。 runtime 最值得一提的变化是新增了runtime.Pinner。 它的能力是可以让某个go的对象不会gc回收,一直到Unpin方法被调用。这个是为了方便cgo代码里让c使用go的对象而设计的。 不要滥用这个接口,如果想告诉gc某个对象暂时不能回收,应该正确使用runtime.KeepAlive。 runtime/trace现在有了很大的性能提升,因此观察程序行为的时候开销更小,更接近程序真实的负载。 sync 添加了OnceFunc、OnceValue、OnceValues这三个帮助函数。主要是为了简化代码。 1.21前: 1 2 3 4 5 6 7 8 var initFlag sync.Once func GetSomeThing() { initFlag.Do(func(){ 真正的初始化逻辑 }) // 后续处理 } 现在变成: 1 2 3 4 5 6 7 8 var doInit = sync.OnceFunc(func(){ 真正的初始化逻辑 }) func GetSomeThing() { doInit() // 后续处理 } 新代码要简单点。 OnceValue、OnceValues是函数带返回值的版本,支持一个和两个返回值的函数。 errors 新增了errors.ErrUnsupported。这个错误表示当前操作系统、硬件、协议、或者文件系统不支持某种操作。 目前os,filepath,syscall,io里的一些函数已经会返回这个错误,可以用errors.Is(err, errors.ErrUnsupported)来检查。 unicode 升级到了最新的Unicode 15.0.0。 平台支持变化 新增了wasip1支持,这是一个对WASI(WebAssembly System Interface)的初步支持。 对于macOS,go1.21需要macOS 10.15 Catalina及以上版本。 龙芯上golang现在支持将代码编译为c的动态和静态链接库,基本上在龙芯上已经可以尝试投入生产环境了。 发版日志 Go 1.21 Release Notes - The Go Programming Language

2023/10/20
articleCard.readMore